bpo-36390: IDLE: Combine region formatting methods. (GH-12481)
Rename paragraph.py to format.py and add region formatting methods from editor.py. Add tests for the latter.
This commit is contained in:
parent
fb26504d14
commit
82494aa6d9
|
@ -29,7 +29,7 @@ from idlelib.textview import view_text
|
|||
from idlelib.autocomplete import AutoComplete
|
||||
from idlelib.codecontext import CodeContext
|
||||
from idlelib.parenmatch import ParenMatch
|
||||
from idlelib.paragraph import FormatParagraph
|
||||
from idlelib.format import FormatParagraph
|
||||
from idlelib.squeezer import Squeezer
|
||||
|
||||
changes = ConfigChanges()
|
||||
|
|
|
@ -53,7 +53,7 @@ class EditorWindow(object):
|
|||
from idlelib.autoexpand import AutoExpand
|
||||
from idlelib.calltip import Calltip
|
||||
from idlelib.codecontext import CodeContext
|
||||
from idlelib.paragraph import FormatParagraph
|
||||
from idlelib.format import FormatParagraph, FormatRegion
|
||||
from idlelib.parenmatch import ParenMatch
|
||||
from idlelib.rstrip import Rstrip
|
||||
from idlelib.squeezer import Squeezer
|
||||
|
@ -172,13 +172,14 @@ class EditorWindow(object):
|
|||
text.bind("<<smart-backspace>>",self.smart_backspace_event)
|
||||
text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
|
||||
text.bind("<<smart-indent>>",self.smart_indent_event)
|
||||
text.bind("<<indent-region>>",self.indent_region_event)
|
||||
text.bind("<<dedent-region>>",self.dedent_region_event)
|
||||
text.bind("<<comment-region>>",self.comment_region_event)
|
||||
text.bind("<<uncomment-region>>",self.uncomment_region_event)
|
||||
text.bind("<<tabify-region>>",self.tabify_region_event)
|
||||
text.bind("<<untabify-region>>",self.untabify_region_event)
|
||||
text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
|
||||
self.fregion = fregion = self.FormatRegion(self)
|
||||
text.bind("<<indent-region>>", fregion.indent_region_event)
|
||||
text.bind("<<dedent-region>>", fregion.dedent_region_event)
|
||||
text.bind("<<comment-region>>", fregion.comment_region_event)
|
||||
text.bind("<<uncomment-region>>", fregion.uncomment_region_event)
|
||||
text.bind("<<tabify-region>>", fregion.tabify_region_event)
|
||||
text.bind("<<untabify-region>>", fregion.untabify_region_event)
|
||||
text.bind("<<toggle-tabs>>", self.toggle_tabs_event)
|
||||
text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
|
||||
text.bind("<Left>", self.move_at_edge_if_selection(0))
|
||||
text.bind("<Right>", self.move_at_edge_if_selection(1))
|
||||
|
@ -1290,7 +1291,7 @@ class EditorWindow(object):
|
|||
try:
|
||||
if first and last:
|
||||
if index2line(first) != index2line(last):
|
||||
return self.indent_region_event(event)
|
||||
return self.fregion.indent_region_event(event)
|
||||
text.delete(first, last)
|
||||
text.mark_set("insert", first)
|
||||
prefix = text.get("insert linestart", "insert")
|
||||
|
@ -1423,72 +1424,6 @@ class EditorWindow(object):
|
|||
return _icis(_startindex + "+%dc" % offset)
|
||||
return inner
|
||||
|
||||
def indent_region_event(self, event):
|
||||
head, tail, chars, lines = self.get_region()
|
||||
for pos in range(len(lines)):
|
||||
line = lines[pos]
|
||||
if line:
|
||||
raw, effective = get_line_indent(line, self.tabwidth)
|
||||
effective = effective + self.indentwidth
|
||||
lines[pos] = self._make_blanks(effective) + line[raw:]
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def dedent_region_event(self, event):
|
||||
head, tail, chars, lines = self.get_region()
|
||||
for pos in range(len(lines)):
|
||||
line = lines[pos]
|
||||
if line:
|
||||
raw, effective = get_line_indent(line, self.tabwidth)
|
||||
effective = max(effective - self.indentwidth, 0)
|
||||
lines[pos] = self._make_blanks(effective) + line[raw:]
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def comment_region_event(self, event):
|
||||
head, tail, chars, lines = self.get_region()
|
||||
for pos in range(len(lines) - 1):
|
||||
line = lines[pos]
|
||||
lines[pos] = '##' + line
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def uncomment_region_event(self, event):
|
||||
head, tail, chars, lines = self.get_region()
|
||||
for pos in range(len(lines)):
|
||||
line = lines[pos]
|
||||
if not line:
|
||||
continue
|
||||
if line[:2] == '##':
|
||||
line = line[2:]
|
||||
elif line[:1] == '#':
|
||||
line = line[1:]
|
||||
lines[pos] = line
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def tabify_region_event(self, event):
|
||||
head, tail, chars, lines = self.get_region()
|
||||
tabwidth = self._asktabwidth()
|
||||
if tabwidth is None: return
|
||||
for pos in range(len(lines)):
|
||||
line = lines[pos]
|
||||
if line:
|
||||
raw, effective = get_line_indent(line, tabwidth)
|
||||
ntabs, nspaces = divmod(effective, tabwidth)
|
||||
lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def untabify_region_event(self, event):
|
||||
head, tail, chars, lines = self.get_region()
|
||||
tabwidth = self._asktabwidth()
|
||||
if tabwidth is None: return
|
||||
for pos in range(len(lines)):
|
||||
lines[pos] = lines[pos].expandtabs(tabwidth)
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def toggle_tabs_event(self, event):
|
||||
if self.askyesno(
|
||||
"Toggle tabs",
|
||||
|
@ -1523,33 +1458,6 @@ class EditorWindow(object):
|
|||
self.indentwidth = new
|
||||
return "break"
|
||||
|
||||
def get_region(self):
|
||||
text = self.text
|
||||
first, last = self.get_selection_indices()
|
||||
if first and last:
|
||||
head = text.index(first + " linestart")
|
||||
tail = text.index(last + "-1c lineend +1c")
|
||||
else:
|
||||
head = text.index("insert linestart")
|
||||
tail = text.index("insert lineend +1c")
|
||||
chars = text.get(head, tail)
|
||||
lines = chars.split("\n")
|
||||
return head, tail, chars, lines
|
||||
|
||||
def set_region(self, head, tail, chars, lines):
|
||||
text = self.text
|
||||
newchars = "\n".join(lines)
|
||||
if newchars == chars:
|
||||
text.bell()
|
||||
return
|
||||
text.tag_remove("sel", "1.0", "end")
|
||||
text.mark_set("insert", head)
|
||||
text.undo_block_start()
|
||||
text.delete(head, tail)
|
||||
text.insert(head, newchars)
|
||||
text.undo_block_stop()
|
||||
text.tag_add("sel", head, "insert")
|
||||
|
||||
# Make string that displays as n leading blanks.
|
||||
|
||||
def _make_blanks(self, n):
|
||||
|
@ -1571,15 +1479,6 @@ class EditorWindow(object):
|
|||
text.insert("insert", self._make_blanks(column))
|
||||
text.undo_block_stop()
|
||||
|
||||
def _asktabwidth(self):
|
||||
return self.askinteger(
|
||||
"Tab width",
|
||||
"Columns per tab? (2-16)",
|
||||
parent=self.text,
|
||||
initialvalue=self.indentwidth,
|
||||
minvalue=2,
|
||||
maxvalue=16)
|
||||
|
||||
# Guess indentwidth from text content.
|
||||
# Return guessed indentwidth. This should not be believed unless
|
||||
# it's in a reasonable range (e.g., it will be 0 if no indented
|
||||
|
|
|
@ -0,0 +1,357 @@
|
|||
"""Format all or a selected region (line slice) of text.
|
||||
|
||||
Region formatting options: paragraph, comment block, indent, deindent,
|
||||
comment, uncomment, tabify, and untabify.
|
||||
|
||||
File renamed from paragraph.py with functions added from editor.py.
|
||||
"""
|
||||
import re
|
||||
from tkinter.simpledialog import askinteger
|
||||
from idlelib.config import idleConf
|
||||
|
||||
|
||||
class FormatParagraph:
|
||||
"""Format a paragraph, comment block, or selection to a max width.
|
||||
|
||||
Does basic, standard text formatting, and also understands Python
|
||||
comment blocks. Thus, for editing Python source code, this
|
||||
extension is really only suitable for reformatting these comment
|
||||
blocks or triple-quoted strings.
|
||||
|
||||
Known problems with comment reformatting:
|
||||
* If there is a selection marked, and the first line of the
|
||||
selection is not complete, the block will probably not be detected
|
||||
as comments, and will have the normal "text formatting" rules
|
||||
applied.
|
||||
* If a comment block has leading whitespace that mixes tabs and
|
||||
spaces, they will not be considered part of the same block.
|
||||
* Fancy comments, like this bulleted list, aren't handled :-)
|
||||
"""
|
||||
def __init__(self, editwin):
|
||||
self.editwin = editwin
|
||||
|
||||
@classmethod
|
||||
def reload(cls):
|
||||
cls.max_width = idleConf.GetOption('extensions', 'FormatParagraph',
|
||||
'max-width', type='int', default=72)
|
||||
|
||||
def close(self):
|
||||
self.editwin = None
|
||||
|
||||
def format_paragraph_event(self, event, limit=None):
|
||||
"""Formats paragraph to a max width specified in idleConf.
|
||||
|
||||
If text is selected, format_paragraph_event will start breaking lines
|
||||
at the max width, starting from the beginning selection.
|
||||
|
||||
If no text is selected, format_paragraph_event uses the current
|
||||
cursor location to determine the paragraph (lines of text surrounded
|
||||
by blank lines) and formats it.
|
||||
|
||||
The length limit parameter is for testing with a known value.
|
||||
"""
|
||||
limit = self.max_width if limit is None else limit
|
||||
text = self.editwin.text
|
||||
first, last = self.editwin.get_selection_indices()
|
||||
if first and last:
|
||||
data = text.get(first, last)
|
||||
comment_header = get_comment_header(data)
|
||||
else:
|
||||
first, last, comment_header, data = \
|
||||
find_paragraph(text, text.index("insert"))
|
||||
if comment_header:
|
||||
newdata = reformat_comment(data, limit, comment_header)
|
||||
else:
|
||||
newdata = reformat_paragraph(data, limit)
|
||||
text.tag_remove("sel", "1.0", "end")
|
||||
|
||||
if newdata != data:
|
||||
text.mark_set("insert", first)
|
||||
text.undo_block_start()
|
||||
text.delete(first, last)
|
||||
text.insert(first, newdata)
|
||||
text.undo_block_stop()
|
||||
else:
|
||||
text.mark_set("insert", last)
|
||||
text.see("insert")
|
||||
return "break"
|
||||
|
||||
|
||||
FormatParagraph.reload()
|
||||
|
||||
def find_paragraph(text, mark):
|
||||
"""Returns the start/stop indices enclosing the paragraph that mark is in.
|
||||
|
||||
Also returns the comment format string, if any, and paragraph of text
|
||||
between the start/stop indices.
|
||||
"""
|
||||
lineno, col = map(int, mark.split("."))
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
|
||||
# Look for start of next paragraph if the index passed in is a blank line
|
||||
while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
|
||||
lineno = lineno + 1
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
first_lineno = lineno
|
||||
comment_header = get_comment_header(line)
|
||||
comment_header_len = len(comment_header)
|
||||
|
||||
# Once start line found, search for end of paragraph (a blank line)
|
||||
while get_comment_header(line)==comment_header and \
|
||||
not is_all_white(line[comment_header_len:]):
|
||||
lineno = lineno + 1
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
last = "%d.0" % lineno
|
||||
|
||||
# Search back to beginning of paragraph (first blank line before)
|
||||
lineno = first_lineno - 1
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
while lineno > 0 and \
|
||||
get_comment_header(line)==comment_header and \
|
||||
not is_all_white(line[comment_header_len:]):
|
||||
lineno = lineno - 1
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
first = "%d.0" % (lineno+1)
|
||||
|
||||
return first, last, comment_header, text.get(first, last)
|
||||
|
||||
# This should perhaps be replaced with textwrap.wrap
|
||||
def reformat_paragraph(data, limit):
|
||||
"""Return data reformatted to specified width (limit)."""
|
||||
lines = data.split("\n")
|
||||
i = 0
|
||||
n = len(lines)
|
||||
while i < n and is_all_white(lines[i]):
|
||||
i = i+1
|
||||
if i >= n:
|
||||
return data
|
||||
indent1 = get_indent(lines[i])
|
||||
if i+1 < n and not is_all_white(lines[i+1]):
|
||||
indent2 = get_indent(lines[i+1])
|
||||
else:
|
||||
indent2 = indent1
|
||||
new = lines[:i]
|
||||
partial = indent1
|
||||
while i < n and not is_all_white(lines[i]):
|
||||
# XXX Should take double space after period (etc.) into account
|
||||
words = re.split(r"(\s+)", lines[i])
|
||||
for j in range(0, len(words), 2):
|
||||
word = words[j]
|
||||
if not word:
|
||||
continue # Can happen when line ends in whitespace
|
||||
if len((partial + word).expandtabs()) > limit and \
|
||||
partial != indent1:
|
||||
new.append(partial.rstrip())
|
||||
partial = indent2
|
||||
partial = partial + word + " "
|
||||
if j+1 < len(words) and words[j+1] != " ":
|
||||
partial = partial + " "
|
||||
i = i+1
|
||||
new.append(partial.rstrip())
|
||||
# XXX Should reformat remaining paragraphs as well
|
||||
new.extend(lines[i:])
|
||||
return "\n".join(new)
|
||||
|
||||
def reformat_comment(data, limit, comment_header):
|
||||
"""Return data reformatted to specified width with comment header."""
|
||||
|
||||
# Remove header from the comment lines
|
||||
lc = len(comment_header)
|
||||
data = "\n".join(line[lc:] for line in data.split("\n"))
|
||||
# Reformat to maxformatwidth chars or a 20 char width,
|
||||
# whichever is greater.
|
||||
format_width = max(limit - len(comment_header), 20)
|
||||
newdata = reformat_paragraph(data, format_width)
|
||||
# re-split and re-insert the comment header.
|
||||
newdata = newdata.split("\n")
|
||||
# If the block ends in a \n, we don't want the comment prefix
|
||||
# inserted after it. (Im not sure it makes sense to reformat a
|
||||
# comment block that is not made of complete lines, but whatever!)
|
||||
# Can't think of a clean solution, so we hack away
|
||||
block_suffix = ""
|
||||
if not newdata[-1]:
|
||||
block_suffix = "\n"
|
||||
newdata = newdata[:-1]
|
||||
return '\n'.join(comment_header+line for line in newdata) + block_suffix
|
||||
|
||||
def is_all_white(line):
|
||||
"""Return True if line is empty or all whitespace."""
|
||||
|
||||
return re.match(r"^\s*$", line) is not None
|
||||
|
||||
def get_indent(line):
|
||||
"""Return the initial space or tab indent of line."""
|
||||
return re.match(r"^([ \t]*)", line).group()
|
||||
|
||||
def get_comment_header(line):
|
||||
"""Return string with leading whitespace and '#' from line or ''.
|
||||
|
||||
A null return indicates that the line is not a comment line. A non-
|
||||
null return, such as ' #', will be used to find the other lines of
|
||||
a comment block with the same indent.
|
||||
"""
|
||||
m = re.match(r"^([ \t]*#*)", line)
|
||||
if m is None: return ""
|
||||
return m.group(1)
|
||||
|
||||
|
||||
# Copy from editor.py; importing it would cause an import cycle.
|
||||
_line_indent_re = re.compile(r'[ \t]*')
|
||||
|
||||
def get_line_indent(line, tabwidth):
|
||||
"""Return a line's indentation as (# chars, effective # of spaces).
|
||||
|
||||
The effective # of spaces is the length after properly "expanding"
|
||||
the tabs into spaces, as done by str.expandtabs(tabwidth).
|
||||
"""
|
||||
m = _line_indent_re.match(line)
|
||||
return m.end(), len(m.group().expandtabs(tabwidth))
|
||||
|
||||
|
||||
class FormatRegion:
|
||||
"Format selected text."
|
||||
|
||||
def __init__(self, editwin):
|
||||
self.editwin = editwin
|
||||
|
||||
def get_region(self):
|
||||
"""Return line information about the selected text region.
|
||||
|
||||
If text is selected, the first and last indices will be
|
||||
for the selection. If there is no text selected, the
|
||||
indices will be the current cursor location.
|
||||
|
||||
Return a tuple containing (first index, last index,
|
||||
string representation of text, list of text lines).
|
||||
"""
|
||||
text = self.editwin.text
|
||||
first, last = self.editwin.get_selection_indices()
|
||||
if first and last:
|
||||
head = text.index(first + " linestart")
|
||||
tail = text.index(last + "-1c lineend +1c")
|
||||
else:
|
||||
head = text.index("insert linestart")
|
||||
tail = text.index("insert lineend +1c")
|
||||
chars = text.get(head, tail)
|
||||
lines = chars.split("\n")
|
||||
return head, tail, chars, lines
|
||||
|
||||
def set_region(self, head, tail, chars, lines):
|
||||
"""Replace the text between the given indices.
|
||||
|
||||
Args:
|
||||
head: Starting index of text to replace.
|
||||
tail: Ending index of text to replace.
|
||||
chars: Expected to be string of current text
|
||||
between head and tail.
|
||||
lines: List of new lines to insert between head
|
||||
and tail.
|
||||
"""
|
||||
text = self.editwin.text
|
||||
newchars = "\n".join(lines)
|
||||
if newchars == chars:
|
||||
text.bell()
|
||||
return
|
||||
text.tag_remove("sel", "1.0", "end")
|
||||
text.mark_set("insert", head)
|
||||
text.undo_block_start()
|
||||
text.delete(head, tail)
|
||||
text.insert(head, newchars)
|
||||
text.undo_block_stop()
|
||||
text.tag_add("sel", head, "insert")
|
||||
|
||||
def indent_region_event(self, event=None):
|
||||
"Indent region by indentwidth spaces."
|
||||
head, tail, chars, lines = self.get_region()
|
||||
for pos in range(len(lines)):
|
||||
line = lines[pos]
|
||||
if line:
|
||||
raw, effective = get_line_indent(line, self.editwin.tabwidth)
|
||||
effective = effective + self.editwin.indentwidth
|
||||
lines[pos] = self.editwin._make_blanks(effective) + line[raw:]
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def dedent_region_event(self, event=None):
|
||||
"Dedent region by indentwidth spaces."
|
||||
head, tail, chars, lines = self.get_region()
|
||||
for pos in range(len(lines)):
|
||||
line = lines[pos]
|
||||
if line:
|
||||
raw, effective = get_line_indent(line, self.editwin.tabwidth)
|
||||
effective = max(effective - self.editwin.indentwidth, 0)
|
||||
lines[pos] = self.editwin._make_blanks(effective) + line[raw:]
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def comment_region_event(self, event=None):
|
||||
"""Comment out each line in region.
|
||||
|
||||
## is appended to the beginning of each line to comment it out.
|
||||
"""
|
||||
head, tail, chars, lines = self.get_region()
|
||||
for pos in range(len(lines) - 1):
|
||||
line = lines[pos]
|
||||
lines[pos] = '##' + line
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def uncomment_region_event(self, event=None):
|
||||
"""Uncomment each line in region.
|
||||
|
||||
Remove ## or # in the first positions of a line. If the comment
|
||||
is not in the beginning position, this command will have no effect.
|
||||
"""
|
||||
head, tail, chars, lines = self.get_region()
|
||||
for pos in range(len(lines)):
|
||||
line = lines[pos]
|
||||
if not line:
|
||||
continue
|
||||
if line[:2] == '##':
|
||||
line = line[2:]
|
||||
elif line[:1] == '#':
|
||||
line = line[1:]
|
||||
lines[pos] = line
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def tabify_region_event(self, event=None):
|
||||
"Convert leading spaces to tabs for each line in selected region."
|
||||
head, tail, chars, lines = self.get_region()
|
||||
tabwidth = self._asktabwidth()
|
||||
if tabwidth is None:
|
||||
return
|
||||
for pos in range(len(lines)):
|
||||
line = lines[pos]
|
||||
if line:
|
||||
raw, effective = get_line_indent(line, tabwidth)
|
||||
ntabs, nspaces = divmod(effective, tabwidth)
|
||||
lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def untabify_region_event(self, event=None):
|
||||
"Expand tabs to spaces for each line in region."
|
||||
head, tail, chars, lines = self.get_region()
|
||||
tabwidth = self._asktabwidth()
|
||||
if tabwidth is None:
|
||||
return
|
||||
for pos in range(len(lines)):
|
||||
lines[pos] = lines[pos].expandtabs(tabwidth)
|
||||
self.set_region(head, tail, chars, lines)
|
||||
return "break"
|
||||
|
||||
def _asktabwidth(self):
|
||||
"Return value for tab width."
|
||||
return askinteger(
|
||||
"Tab width",
|
||||
"Columns per tab? (2-16)",
|
||||
parent=self.editwin.text,
|
||||
initialvalue=self.editwin.indentwidth,
|
||||
minvalue=2,
|
||||
maxvalue=16)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from unittest import main
|
||||
main('idlelib.idle_test.test_format', verbosity=2, exit=False)
|
|
@ -1,7 +1,8 @@
|
|||
"Test paragraph, coverage 76%."
|
||||
"Test format, coverage 99%."
|
||||
|
||||
from idlelib import paragraph as pg
|
||||
from idlelib import format as ft
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from test.support import requires
|
||||
from tkinter import Tk, Text
|
||||
from idlelib.editor import EditorWindow
|
||||
|
@ -16,26 +17,26 @@ class Is_Get_Test(unittest.TestCase):
|
|||
leadingws_nocomment = ' This is not a comment'
|
||||
|
||||
def test_is_all_white(self):
|
||||
self.assertTrue(pg.is_all_white(''))
|
||||
self.assertTrue(pg.is_all_white('\t\n\r\f\v'))
|
||||
self.assertFalse(pg.is_all_white(self.test_comment))
|
||||
self.assertTrue(ft.is_all_white(''))
|
||||
self.assertTrue(ft.is_all_white('\t\n\r\f\v'))
|
||||
self.assertFalse(ft.is_all_white(self.test_comment))
|
||||
|
||||
def test_get_indent(self):
|
||||
Equal = self.assertEqual
|
||||
Equal(pg.get_indent(self.test_comment), '')
|
||||
Equal(pg.get_indent(self.trailingws_comment), '')
|
||||
Equal(pg.get_indent(self.leadingws_comment), ' ')
|
||||
Equal(pg.get_indent(self.leadingws_nocomment), ' ')
|
||||
Equal(ft.get_indent(self.test_comment), '')
|
||||
Equal(ft.get_indent(self.trailingws_comment), '')
|
||||
Equal(ft.get_indent(self.leadingws_comment), ' ')
|
||||
Equal(ft.get_indent(self.leadingws_nocomment), ' ')
|
||||
|
||||
def test_get_comment_header(self):
|
||||
Equal = self.assertEqual
|
||||
# Test comment strings
|
||||
Equal(pg.get_comment_header(self.test_comment), '#')
|
||||
Equal(pg.get_comment_header(self.trailingws_comment), '#')
|
||||
Equal(pg.get_comment_header(self.leadingws_comment), ' #')
|
||||
Equal(ft.get_comment_header(self.test_comment), '#')
|
||||
Equal(ft.get_comment_header(self.trailingws_comment), '#')
|
||||
Equal(ft.get_comment_header(self.leadingws_comment), ' #')
|
||||
# Test non-comment strings
|
||||
Equal(pg.get_comment_header(self.leadingws_nocomment), ' ')
|
||||
Equal(pg.get_comment_header(self.test_nocomment), '')
|
||||
Equal(ft.get_comment_header(self.leadingws_nocomment), ' ')
|
||||
Equal(ft.get_comment_header(self.test_nocomment), '')
|
||||
|
||||
|
||||
class FindTest(unittest.TestCase):
|
||||
|
@ -63,7 +64,7 @@ class FindTest(unittest.TestCase):
|
|||
linelength = int(text.index("%d.end" % line).split('.')[1])
|
||||
for col in (0, linelength//2, linelength):
|
||||
tempindex = "%d.%d" % (line, col)
|
||||
self.assertEqual(pg.find_paragraph(text, tempindex), expected)
|
||||
self.assertEqual(ft.find_paragraph(text, tempindex), expected)
|
||||
text.delete('1.0', 'end')
|
||||
|
||||
def test_find_comment(self):
|
||||
|
@ -162,7 +163,7 @@ class ReformatFunctionTest(unittest.TestCase):
|
|||
|
||||
def test_reformat_paragraph(self):
|
||||
Equal = self.assertEqual
|
||||
reform = pg.reformat_paragraph
|
||||
reform = ft.reformat_paragraph
|
||||
hw = "O hello world"
|
||||
Equal(reform(' ', 1), ' ')
|
||||
Equal(reform("Hello world", 20), "Hello world")
|
||||
|
@ -193,7 +194,7 @@ class ReformatCommentTest(unittest.TestCase):
|
|||
test_string = (
|
||||
" \"\"\"this is a test of a reformat for a triple quoted string"
|
||||
" will it reformat to less than 70 characters for me?\"\"\"")
|
||||
result = pg.reformat_comment(test_string, 70, " ")
|
||||
result = ft.reformat_comment(test_string, 70, " ")
|
||||
expected = (
|
||||
" \"\"\"this is a test of a reformat for a triple quoted string will it\n"
|
||||
" reformat to less than 70 characters for me?\"\"\"")
|
||||
|
@ -202,7 +203,7 @@ class ReformatCommentTest(unittest.TestCase):
|
|||
test_comment = (
|
||||
"# this is a test of a reformat for a triple quoted string will "
|
||||
"it reformat to less than 70 characters for me?")
|
||||
result = pg.reformat_comment(test_comment, 70, "#")
|
||||
result = ft.reformat_comment(test_comment, 70, "#")
|
||||
expected = (
|
||||
"# this is a test of a reformat for a triple quoted string will it\n"
|
||||
"# reformat to less than 70 characters for me?")
|
||||
|
@ -211,7 +212,7 @@ class ReformatCommentTest(unittest.TestCase):
|
|||
|
||||
class FormatClassTest(unittest.TestCase):
|
||||
def test_init_close(self):
|
||||
instance = pg.FormatParagraph('editor')
|
||||
instance = ft.FormatParagraph('editor')
|
||||
self.assertEqual(instance.editwin, 'editor')
|
||||
instance.close()
|
||||
self.assertEqual(instance.editwin, None)
|
||||
|
@ -273,7 +274,7 @@ class FormatEventTest(unittest.TestCase):
|
|||
cls.root.withdraw()
|
||||
editor = Editor(root=cls.root)
|
||||
cls.text = editor.text.text # Test code does not need the wrapper.
|
||||
cls.formatter = pg.FormatParagraph(editor).format_paragraph_event
|
||||
cls.formatter = ft.FormatParagraph(editor).format_paragraph_event
|
||||
# Sets the insert mark just after the re-wrapped and inserted text.
|
||||
|
||||
@classmethod
|
||||
|
@ -375,5 +376,202 @@ class FormatEventTest(unittest.TestCase):
|
|||
## text.delete('1.0', 'end')
|
||||
|
||||
|
||||
class DummyEditwin:
|
||||
def __init__(self, root, text):
|
||||
self.root = root
|
||||
self.text = text
|
||||
self.indentwidth = 4
|
||||
self.tabwidth = 4
|
||||
self.usetabs = False
|
||||
self.context_use_ps1 = True
|
||||
|
||||
_make_blanks = EditorWindow._make_blanks
|
||||
get_selection_indices = EditorWindow.get_selection_indices
|
||||
|
||||
|
||||
class FormatRegionTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
requires('gui')
|
||||
cls.root = Tk()
|
||||
cls.root.withdraw()
|
||||
cls.text = Text(cls.root)
|
||||
cls.text.undo_block_start = mock.Mock()
|
||||
cls.text.undo_block_stop = mock.Mock()
|
||||
cls.editor = DummyEditwin(cls.root, cls.text)
|
||||
cls.formatter = ft.FormatRegion(cls.editor)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
del cls.text, cls.formatter, cls.editor
|
||||
cls.root.update_idletasks()
|
||||
cls.root.destroy()
|
||||
del cls.root
|
||||
|
||||
def setUp(self):
|
||||
self.text.insert('1.0', self.code_sample)
|
||||
|
||||
def tearDown(self):
|
||||
self.text.delete('1.0', 'end')
|
||||
|
||||
code_sample = """\
|
||||
|
||||
class C1():
|
||||
# Class comment.
|
||||
def __init__(self, a, b):
|
||||
self.a = a
|
||||
self.b = b
|
||||
|
||||
def compare(self):
|
||||
if a > b:
|
||||
return a
|
||||
elif a < b:
|
||||
return b
|
||||
else:
|
||||
return None
|
||||
"""
|
||||
|
||||
def test_get_region(self):
|
||||
get = self.formatter.get_region
|
||||
text = self.text
|
||||
eq = self.assertEqual
|
||||
|
||||
# Add selection.
|
||||
text.tag_add('sel', '7.0', '10.0')
|
||||
expected_lines = ['',
|
||||
' def compare(self):',
|
||||
' if a > b:',
|
||||
'']
|
||||
eq(get(), ('7.0', '10.0', '\n'.join(expected_lines), expected_lines))
|
||||
|
||||
# Remove selection.
|
||||
text.tag_remove('sel', '1.0', 'end')
|
||||
eq(get(), ('15.0', '16.0', '\n', ['', '']))
|
||||
|
||||
def test_set_region(self):
|
||||
set_ = self.formatter.set_region
|
||||
text = self.text
|
||||
eq = self.assertEqual
|
||||
|
||||
save_bell = text.bell
|
||||
text.bell = mock.Mock()
|
||||
line6 = self.code_sample.splitlines()[5]
|
||||
line10 = self.code_sample.splitlines()[9]
|
||||
|
||||
text.tag_add('sel', '6.0', '11.0')
|
||||
head, tail, chars, lines = self.formatter.get_region()
|
||||
|
||||
# No changes.
|
||||
set_(head, tail, chars, lines)
|
||||
text.bell.assert_called_once()
|
||||
eq(text.get('6.0', '11.0'), chars)
|
||||
eq(text.get('sel.first', 'sel.last'), chars)
|
||||
text.tag_remove('sel', '1.0', 'end')
|
||||
|
||||
# Alter selected lines by changing lines and adding a newline.
|
||||
newstring = 'added line 1\n\n\n\n'
|
||||
newlines = newstring.split('\n')
|
||||
set_('7.0', '10.0', chars, newlines)
|
||||
# Selection changed.
|
||||
eq(text.get('sel.first', 'sel.last'), newstring)
|
||||
# Additional line added, so last index is changed.
|
||||
eq(text.get('7.0', '11.0'), newstring)
|
||||
# Before and after lines unchanged.
|
||||
eq(text.get('6.0', '7.0-1c'), line6)
|
||||
eq(text.get('11.0', '12.0-1c'), line10)
|
||||
text.tag_remove('sel', '1.0', 'end')
|
||||
|
||||
text.bell = save_bell
|
||||
|
||||
def test_indent_region_event(self):
|
||||
indent = self.formatter.indent_region_event
|
||||
text = self.text
|
||||
eq = self.assertEqual
|
||||
|
||||
text.tag_add('sel', '7.0', '10.0')
|
||||
indent()
|
||||
# Blank lines aren't affected by indent.
|
||||
eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
|
||||
|
||||
def test_dedent_region_event(self):
|
||||
dedent = self.formatter.dedent_region_event
|
||||
text = self.text
|
||||
eq = self.assertEqual
|
||||
|
||||
text.tag_add('sel', '7.0', '10.0')
|
||||
dedent()
|
||||
# Blank lines aren't affected by dedent.
|
||||
eq(text.get('7.0', '10.0'), ('\ndef compare(self):\n if a > b:\n'))
|
||||
|
||||
def test_comment_region_event(self):
|
||||
comment = self.formatter.comment_region_event
|
||||
text = self.text
|
||||
eq = self.assertEqual
|
||||
|
||||
text.tag_add('sel', '7.0', '10.0')
|
||||
comment()
|
||||
eq(text.get('7.0', '10.0'), ('##\n## def compare(self):\n## if a > b:\n'))
|
||||
|
||||
def test_uncomment_region_event(self):
|
||||
comment = self.formatter.comment_region_event
|
||||
uncomment = self.formatter.uncomment_region_event
|
||||
text = self.text
|
||||
eq = self.assertEqual
|
||||
|
||||
text.tag_add('sel', '7.0', '10.0')
|
||||
comment()
|
||||
uncomment()
|
||||
eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
|
||||
|
||||
# Only remove comments at the beginning of a line.
|
||||
text.tag_remove('sel', '1.0', 'end')
|
||||
text.tag_add('sel', '3.0', '4.0')
|
||||
uncomment()
|
||||
eq(text.get('3.0', '3.end'), (' # Class comment.'))
|
||||
|
||||
self.formatter.set_region('3.0', '4.0', '', ['# Class comment.', ''])
|
||||
uncomment()
|
||||
eq(text.get('3.0', '3.end'), (' Class comment.'))
|
||||
|
||||
@mock.patch.object(ft.FormatRegion, "_asktabwidth")
|
||||
def test_tabify_region_event(self, _asktabwidth):
|
||||
tabify = self.formatter.tabify_region_event
|
||||
text = self.text
|
||||
eq = self.assertEqual
|
||||
|
||||
text.tag_add('sel', '7.0', '10.0')
|
||||
# No tabwidth selected.
|
||||
_asktabwidth.return_value = None
|
||||
self.assertIsNone(tabify())
|
||||
|
||||
_asktabwidth.return_value = 3
|
||||
self.assertIsNotNone(tabify())
|
||||
eq(text.get('7.0', '10.0'), ('\n\t def compare(self):\n\t\t if a > b:\n'))
|
||||
|
||||
@mock.patch.object(ft.FormatRegion, "_asktabwidth")
|
||||
def test_untabify_region_event(self, _asktabwidth):
|
||||
untabify = self.formatter.untabify_region_event
|
||||
text = self.text
|
||||
eq = self.assertEqual
|
||||
|
||||
text.tag_add('sel', '7.0', '10.0')
|
||||
# No tabwidth selected.
|
||||
_asktabwidth.return_value = None
|
||||
self.assertIsNone(untabify())
|
||||
|
||||
_asktabwidth.return_value = 2
|
||||
self.formatter.tabify_region_event()
|
||||
_asktabwidth.return_value = 3
|
||||
self.assertIsNotNone(untabify())
|
||||
eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
|
||||
|
||||
@mock.patch.object(ft, "askinteger")
|
||||
def test_ask_tabwidth(self, askinteger):
|
||||
ask = self.formatter._asktabwidth
|
||||
askinteger.return_value = 10
|
||||
self.assertEqual(ask(), 10)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, exit=2)
|
|
@ -60,6 +60,7 @@ menudefs = [
|
|||
]),
|
||||
|
||||
('format', [
|
||||
('F_ormat Paragraph', '<<format-paragraph>>'),
|
||||
('_Indent Region', '<<indent-region>>'),
|
||||
('_Dedent Region', '<<dedent-region>>'),
|
||||
('Comment _Out Region', '<<comment-region>>'),
|
||||
|
@ -68,7 +69,6 @@ menudefs = [
|
|||
('Untabify Region', '<<untabify-region>>'),
|
||||
('Toggle Tabs', '<<toggle-tabs>>'),
|
||||
('New Indent Width', '<<change-indentwidth>>'),
|
||||
('F_ormat Paragraph', '<<format-paragraph>>'),
|
||||
('S_trip Trailing Whitespace', '<<do-rstrip>>'),
|
||||
]),
|
||||
|
||||
|
|
|
@ -1,194 +0,0 @@
|
|||
"""Format a paragraph, comment block, or selection to a max width.
|
||||
|
||||
Does basic, standard text formatting, and also understands Python
|
||||
comment blocks. Thus, for editing Python source code, this
|
||||
extension is really only suitable for reformatting these comment
|
||||
blocks or triple-quoted strings.
|
||||
|
||||
Known problems with comment reformatting:
|
||||
* If there is a selection marked, and the first line of the
|
||||
selection is not complete, the block will probably not be detected
|
||||
as comments, and will have the normal "text formatting" rules
|
||||
applied.
|
||||
* If a comment block has leading whitespace that mixes tabs and
|
||||
spaces, they will not be considered part of the same block.
|
||||
* Fancy comments, like this bulleted list, aren't handled :-)
|
||||
"""
|
||||
import re
|
||||
|
||||
from idlelib.config import idleConf
|
||||
|
||||
|
||||
class FormatParagraph:
|
||||
|
||||
def __init__(self, editwin):
|
||||
self.editwin = editwin
|
||||
|
||||
@classmethod
|
||||
def reload(cls):
|
||||
cls.max_width = idleConf.GetOption('extensions', 'FormatParagraph',
|
||||
'max-width', type='int', default=72)
|
||||
|
||||
def close(self):
|
||||
self.editwin = None
|
||||
|
||||
def format_paragraph_event(self, event, limit=None):
|
||||
"""Formats paragraph to a max width specified in idleConf.
|
||||
|
||||
If text is selected, format_paragraph_event will start breaking lines
|
||||
at the max width, starting from the beginning selection.
|
||||
|
||||
If no text is selected, format_paragraph_event uses the current
|
||||
cursor location to determine the paragraph (lines of text surrounded
|
||||
by blank lines) and formats it.
|
||||
|
||||
The length limit parameter is for testing with a known value.
|
||||
"""
|
||||
limit = self.max_width if limit is None else limit
|
||||
text = self.editwin.text
|
||||
first, last = self.editwin.get_selection_indices()
|
||||
if first and last:
|
||||
data = text.get(first, last)
|
||||
comment_header = get_comment_header(data)
|
||||
else:
|
||||
first, last, comment_header, data = \
|
||||
find_paragraph(text, text.index("insert"))
|
||||
if comment_header:
|
||||
newdata = reformat_comment(data, limit, comment_header)
|
||||
else:
|
||||
newdata = reformat_paragraph(data, limit)
|
||||
text.tag_remove("sel", "1.0", "end")
|
||||
|
||||
if newdata != data:
|
||||
text.mark_set("insert", first)
|
||||
text.undo_block_start()
|
||||
text.delete(first, last)
|
||||
text.insert(first, newdata)
|
||||
text.undo_block_stop()
|
||||
else:
|
||||
text.mark_set("insert", last)
|
||||
text.see("insert")
|
||||
return "break"
|
||||
|
||||
|
||||
FormatParagraph.reload()
|
||||
|
||||
def find_paragraph(text, mark):
|
||||
"""Returns the start/stop indices enclosing the paragraph that mark is in.
|
||||
|
||||
Also returns the comment format string, if any, and paragraph of text
|
||||
between the start/stop indices.
|
||||
"""
|
||||
lineno, col = map(int, mark.split("."))
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
|
||||
# Look for start of next paragraph if the index passed in is a blank line
|
||||
while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
|
||||
lineno = lineno + 1
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
first_lineno = lineno
|
||||
comment_header = get_comment_header(line)
|
||||
comment_header_len = len(comment_header)
|
||||
|
||||
# Once start line found, search for end of paragraph (a blank line)
|
||||
while get_comment_header(line)==comment_header and \
|
||||
not is_all_white(line[comment_header_len:]):
|
||||
lineno = lineno + 1
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
last = "%d.0" % lineno
|
||||
|
||||
# Search back to beginning of paragraph (first blank line before)
|
||||
lineno = first_lineno - 1
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
while lineno > 0 and \
|
||||
get_comment_header(line)==comment_header and \
|
||||
not is_all_white(line[comment_header_len:]):
|
||||
lineno = lineno - 1
|
||||
line = text.get("%d.0" % lineno, "%d.end" % lineno)
|
||||
first = "%d.0" % (lineno+1)
|
||||
|
||||
return first, last, comment_header, text.get(first, last)
|
||||
|
||||
# This should perhaps be replaced with textwrap.wrap
|
||||
def reformat_paragraph(data, limit):
|
||||
"""Return data reformatted to specified width (limit)."""
|
||||
lines = data.split("\n")
|
||||
i = 0
|
||||
n = len(lines)
|
||||
while i < n and is_all_white(lines[i]):
|
||||
i = i+1
|
||||
if i >= n:
|
||||
return data
|
||||
indent1 = get_indent(lines[i])
|
||||
if i+1 < n and not is_all_white(lines[i+1]):
|
||||
indent2 = get_indent(lines[i+1])
|
||||
else:
|
||||
indent2 = indent1
|
||||
new = lines[:i]
|
||||
partial = indent1
|
||||
while i < n and not is_all_white(lines[i]):
|
||||
# XXX Should take double space after period (etc.) into account
|
||||
words = re.split(r"(\s+)", lines[i])
|
||||
for j in range(0, len(words), 2):
|
||||
word = words[j]
|
||||
if not word:
|
||||
continue # Can happen when line ends in whitespace
|
||||
if len((partial + word).expandtabs()) > limit and \
|
||||
partial != indent1:
|
||||
new.append(partial.rstrip())
|
||||
partial = indent2
|
||||
partial = partial + word + " "
|
||||
if j+1 < len(words) and words[j+1] != " ":
|
||||
partial = partial + " "
|
||||
i = i+1
|
||||
new.append(partial.rstrip())
|
||||
# XXX Should reformat remaining paragraphs as well
|
||||
new.extend(lines[i:])
|
||||
return "\n".join(new)
|
||||
|
||||
def reformat_comment(data, limit, comment_header):
|
||||
"""Return data reformatted to specified width with comment header."""
|
||||
|
||||
# Remove header from the comment lines
|
||||
lc = len(comment_header)
|
||||
data = "\n".join(line[lc:] for line in data.split("\n"))
|
||||
# Reformat to maxformatwidth chars or a 20 char width,
|
||||
# whichever is greater.
|
||||
format_width = max(limit - len(comment_header), 20)
|
||||
newdata = reformat_paragraph(data, format_width)
|
||||
# re-split and re-insert the comment header.
|
||||
newdata = newdata.split("\n")
|
||||
# If the block ends in a \n, we don't want the comment prefix
|
||||
# inserted after it. (Im not sure it makes sense to reformat a
|
||||
# comment block that is not made of complete lines, but whatever!)
|
||||
# Can't think of a clean solution, so we hack away
|
||||
block_suffix = ""
|
||||
if not newdata[-1]:
|
||||
block_suffix = "\n"
|
||||
newdata = newdata[:-1]
|
||||
return '\n'.join(comment_header+line for line in newdata) + block_suffix
|
||||
|
||||
def is_all_white(line):
|
||||
"""Return True if line is empty or all whitespace."""
|
||||
|
||||
return re.match(r"^\s*$", line) is not None
|
||||
|
||||
def get_indent(line):
|
||||
"""Return the initial space or tab indent of line."""
|
||||
return re.match(r"^([ \t]*)", line).group()
|
||||
|
||||
def get_comment_header(line):
|
||||
"""Return string with leading whitespace and '#' from line or ''.
|
||||
|
||||
A null return indicates that the line is not a comment line. A non-
|
||||
null return, such as ' #', will be used to find the other lines of
|
||||
a comment block with the same indent.
|
||||
"""
|
||||
m = re.match(r"^([ \t]*#*)", line)
|
||||
if m is None: return ""
|
||||
return m.group(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from unittest import main
|
||||
main('idlelib.idle_test.test_paragraph', verbosity=2, exit=False)
|
|
@ -0,0 +1,2 @@
|
|||
Rename paragraph.py to format.py and add region formatting methods
|
||||
from editor.py. Add tests for the latter.
|
Loading…
Reference in New Issue