mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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
7 changed files with 589 additions and 327 deletions
|
@ -29,7 +29,7 @@ from idlelib.textview import view_text
|
||||||
from idlelib.autocomplete import AutoComplete
|
from idlelib.autocomplete import AutoComplete
|
||||||
from idlelib.codecontext import CodeContext
|
from idlelib.codecontext import CodeContext
|
||||||
from idlelib.parenmatch import ParenMatch
|
from idlelib.parenmatch import ParenMatch
|
||||||
from idlelib.paragraph import FormatParagraph
|
from idlelib.format import FormatParagraph
|
||||||
from idlelib.squeezer import Squeezer
|
from idlelib.squeezer import Squeezer
|
||||||
|
|
||||||
changes = ConfigChanges()
|
changes = ConfigChanges()
|
||||||
|
|
|
@ -53,7 +53,7 @@ class EditorWindow(object):
|
||||||
from idlelib.autoexpand import AutoExpand
|
from idlelib.autoexpand import AutoExpand
|
||||||
from idlelib.calltip import Calltip
|
from idlelib.calltip import Calltip
|
||||||
from idlelib.codecontext import CodeContext
|
from idlelib.codecontext import CodeContext
|
||||||
from idlelib.paragraph import FormatParagraph
|
from idlelib.format import FormatParagraph, FormatRegion
|
||||||
from idlelib.parenmatch import ParenMatch
|
from idlelib.parenmatch import ParenMatch
|
||||||
from idlelib.rstrip import Rstrip
|
from idlelib.rstrip import Rstrip
|
||||||
from idlelib.squeezer import Squeezer
|
from idlelib.squeezer import Squeezer
|
||||||
|
@ -172,13 +172,14 @@ class EditorWindow(object):
|
||||||
text.bind("<<smart-backspace>>",self.smart_backspace_event)
|
text.bind("<<smart-backspace>>",self.smart_backspace_event)
|
||||||
text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
|
text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
|
||||||
text.bind("<<smart-indent>>",self.smart_indent_event)
|
text.bind("<<smart-indent>>",self.smart_indent_event)
|
||||||
text.bind("<<indent-region>>",self.indent_region_event)
|
self.fregion = fregion = self.FormatRegion(self)
|
||||||
text.bind("<<dedent-region>>",self.dedent_region_event)
|
text.bind("<<indent-region>>", fregion.indent_region_event)
|
||||||
text.bind("<<comment-region>>",self.comment_region_event)
|
text.bind("<<dedent-region>>", fregion.dedent_region_event)
|
||||||
text.bind("<<uncomment-region>>",self.uncomment_region_event)
|
text.bind("<<comment-region>>", fregion.comment_region_event)
|
||||||
text.bind("<<tabify-region>>",self.tabify_region_event)
|
text.bind("<<uncomment-region>>", fregion.uncomment_region_event)
|
||||||
text.bind("<<untabify-region>>",self.untabify_region_event)
|
text.bind("<<tabify-region>>", fregion.tabify_region_event)
|
||||||
text.bind("<<toggle-tabs>>",self.toggle_tabs_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("<<change-indentwidth>>",self.change_indentwidth_event)
|
||||||
text.bind("<Left>", self.move_at_edge_if_selection(0))
|
text.bind("<Left>", self.move_at_edge_if_selection(0))
|
||||||
text.bind("<Right>", self.move_at_edge_if_selection(1))
|
text.bind("<Right>", self.move_at_edge_if_selection(1))
|
||||||
|
@ -1290,7 +1291,7 @@ class EditorWindow(object):
|
||||||
try:
|
try:
|
||||||
if first and last:
|
if first and last:
|
||||||
if index2line(first) != index2line(last):
|
if index2line(first) != index2line(last):
|
||||||
return self.indent_region_event(event)
|
return self.fregion.indent_region_event(event)
|
||||||
text.delete(first, last)
|
text.delete(first, last)
|
||||||
text.mark_set("insert", first)
|
text.mark_set("insert", first)
|
||||||
prefix = text.get("insert linestart", "insert")
|
prefix = text.get("insert linestart", "insert")
|
||||||
|
@ -1423,72 +1424,6 @@ class EditorWindow(object):
|
||||||
return _icis(_startindex + "+%dc" % offset)
|
return _icis(_startindex + "+%dc" % offset)
|
||||||
return inner
|
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):
|
def toggle_tabs_event(self, event):
|
||||||
if self.askyesno(
|
if self.askyesno(
|
||||||
"Toggle tabs",
|
"Toggle tabs",
|
||||||
|
@ -1523,33 +1458,6 @@ class EditorWindow(object):
|
||||||
self.indentwidth = new
|
self.indentwidth = new
|
||||||
return "break"
|
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.
|
# Make string that displays as n leading blanks.
|
||||||
|
|
||||||
def _make_blanks(self, n):
|
def _make_blanks(self, n):
|
||||||
|
@ -1571,15 +1479,6 @@ class EditorWindow(object):
|
||||||
text.insert("insert", self._make_blanks(column))
|
text.insert("insert", self._make_blanks(column))
|
||||||
text.undo_block_stop()
|
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.
|
# Guess indentwidth from text content.
|
||||||
# Return guessed indentwidth. This should not be believed unless
|
# Return guessed indentwidth. This should not be believed unless
|
||||||
# it's in a reasonable range (e.g., it will be 0 if no indented
|
# it's in a reasonable range (e.g., it will be 0 if no indented
|
||||||
|
|
357
Lib/idlelib/format.py
Normal file
357
Lib/idlelib/format.py
Normal file
|
@ -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
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
from test.support import requires
|
from test.support import requires
|
||||||
from tkinter import Tk, Text
|
from tkinter import Tk, Text
|
||||||
from idlelib.editor import EditorWindow
|
from idlelib.editor import EditorWindow
|
||||||
|
@ -16,26 +17,26 @@ class Is_Get_Test(unittest.TestCase):
|
||||||
leadingws_nocomment = ' This is not a comment'
|
leadingws_nocomment = ' This is not a comment'
|
||||||
|
|
||||||
def test_is_all_white(self):
|
def test_is_all_white(self):
|
||||||
self.assertTrue(pg.is_all_white(''))
|
self.assertTrue(ft.is_all_white(''))
|
||||||
self.assertTrue(pg.is_all_white('\t\n\r\f\v'))
|
self.assertTrue(ft.is_all_white('\t\n\r\f\v'))
|
||||||
self.assertFalse(pg.is_all_white(self.test_comment))
|
self.assertFalse(ft.is_all_white(self.test_comment))
|
||||||
|
|
||||||
def test_get_indent(self):
|
def test_get_indent(self):
|
||||||
Equal = self.assertEqual
|
Equal = self.assertEqual
|
||||||
Equal(pg.get_indent(self.test_comment), '')
|
Equal(ft.get_indent(self.test_comment), '')
|
||||||
Equal(pg.get_indent(self.trailingws_comment), '')
|
Equal(ft.get_indent(self.trailingws_comment), '')
|
||||||
Equal(pg.get_indent(self.leadingws_comment), ' ')
|
Equal(ft.get_indent(self.leadingws_comment), ' ')
|
||||||
Equal(pg.get_indent(self.leadingws_nocomment), ' ')
|
Equal(ft.get_indent(self.leadingws_nocomment), ' ')
|
||||||
|
|
||||||
def test_get_comment_header(self):
|
def test_get_comment_header(self):
|
||||||
Equal = self.assertEqual
|
Equal = self.assertEqual
|
||||||
# Test comment strings
|
# Test comment strings
|
||||||
Equal(pg.get_comment_header(self.test_comment), '#')
|
Equal(ft.get_comment_header(self.test_comment), '#')
|
||||||
Equal(pg.get_comment_header(self.trailingws_comment), '#')
|
Equal(ft.get_comment_header(self.trailingws_comment), '#')
|
||||||
Equal(pg.get_comment_header(self.leadingws_comment), ' #')
|
Equal(ft.get_comment_header(self.leadingws_comment), ' #')
|
||||||
# Test non-comment strings
|
# Test non-comment strings
|
||||||
Equal(pg.get_comment_header(self.leadingws_nocomment), ' ')
|
Equal(ft.get_comment_header(self.leadingws_nocomment), ' ')
|
||||||
Equal(pg.get_comment_header(self.test_nocomment), '')
|
Equal(ft.get_comment_header(self.test_nocomment), '')
|
||||||
|
|
||||||
|
|
||||||
class FindTest(unittest.TestCase):
|
class FindTest(unittest.TestCase):
|
||||||
|
@ -63,7 +64,7 @@ class FindTest(unittest.TestCase):
|
||||||
linelength = int(text.index("%d.end" % line).split('.')[1])
|
linelength = int(text.index("%d.end" % line).split('.')[1])
|
||||||
for col in (0, linelength//2, linelength):
|
for col in (0, linelength//2, linelength):
|
||||||
tempindex = "%d.%d" % (line, col)
|
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')
|
text.delete('1.0', 'end')
|
||||||
|
|
||||||
def test_find_comment(self):
|
def test_find_comment(self):
|
||||||
|
@ -162,7 +163,7 @@ class ReformatFunctionTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_reformat_paragraph(self):
|
def test_reformat_paragraph(self):
|
||||||
Equal = self.assertEqual
|
Equal = self.assertEqual
|
||||||
reform = pg.reformat_paragraph
|
reform = ft.reformat_paragraph
|
||||||
hw = "O hello world"
|
hw = "O hello world"
|
||||||
Equal(reform(' ', 1), ' ')
|
Equal(reform(' ', 1), ' ')
|
||||||
Equal(reform("Hello world", 20), "Hello world")
|
Equal(reform("Hello world", 20), "Hello world")
|
||||||
|
@ -193,7 +194,7 @@ class ReformatCommentTest(unittest.TestCase):
|
||||||
test_string = (
|
test_string = (
|
||||||
" \"\"\"this is a test of a reformat for a triple quoted string"
|
" \"\"\"this is a test of a reformat for a triple quoted string"
|
||||||
" will it reformat to less than 70 characters for me?\"\"\"")
|
" 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 = (
|
expected = (
|
||||||
" \"\"\"this is a test of a reformat for a triple quoted string will it\n"
|
" \"\"\"this is a test of a reformat for a triple quoted string will it\n"
|
||||||
" reformat to less than 70 characters for me?\"\"\"")
|
" reformat to less than 70 characters for me?\"\"\"")
|
||||||
|
@ -202,7 +203,7 @@ class ReformatCommentTest(unittest.TestCase):
|
||||||
test_comment = (
|
test_comment = (
|
||||||
"# this is a test of a reformat for a triple quoted string will "
|
"# this is a test of a reformat for a triple quoted string will "
|
||||||
"it reformat to less than 70 characters for me?")
|
"it reformat to less than 70 characters for me?")
|
||||||
result = pg.reformat_comment(test_comment, 70, "#")
|
result = ft.reformat_comment(test_comment, 70, "#")
|
||||||
expected = (
|
expected = (
|
||||||
"# this is a test of a reformat for a triple quoted string will it\n"
|
"# this is a test of a reformat for a triple quoted string will it\n"
|
||||||
"# reformat to less than 70 characters for me?")
|
"# reformat to less than 70 characters for me?")
|
||||||
|
@ -211,7 +212,7 @@ class ReformatCommentTest(unittest.TestCase):
|
||||||
|
|
||||||
class FormatClassTest(unittest.TestCase):
|
class FormatClassTest(unittest.TestCase):
|
||||||
def test_init_close(self):
|
def test_init_close(self):
|
||||||
instance = pg.FormatParagraph('editor')
|
instance = ft.FormatParagraph('editor')
|
||||||
self.assertEqual(instance.editwin, 'editor')
|
self.assertEqual(instance.editwin, 'editor')
|
||||||
instance.close()
|
instance.close()
|
||||||
self.assertEqual(instance.editwin, None)
|
self.assertEqual(instance.editwin, None)
|
||||||
|
@ -273,7 +274,7 @@ class FormatEventTest(unittest.TestCase):
|
||||||
cls.root.withdraw()
|
cls.root.withdraw()
|
||||||
editor = Editor(root=cls.root)
|
editor = Editor(root=cls.root)
|
||||||
cls.text = editor.text.text # Test code does not need the wrapper.
|
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.
|
# Sets the insert mark just after the re-wrapped and inserted text.
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -375,5 +376,202 @@ class FormatEventTest(unittest.TestCase):
|
||||||
## text.delete('1.0', 'end')
|
## 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__':
|
if __name__ == '__main__':
|
||||||
unittest.main(verbosity=2, exit=2)
|
unittest.main(verbosity=2, exit=2)
|
|
@ -60,6 +60,7 @@ menudefs = [
|
||||||
]),
|
]),
|
||||||
|
|
||||||
('format', [
|
('format', [
|
||||||
|
('F_ormat Paragraph', '<<format-paragraph>>'),
|
||||||
('_Indent Region', '<<indent-region>>'),
|
('_Indent Region', '<<indent-region>>'),
|
||||||
('_Dedent Region', '<<dedent-region>>'),
|
('_Dedent Region', '<<dedent-region>>'),
|
||||||
('Comment _Out Region', '<<comment-region>>'),
|
('Comment _Out Region', '<<comment-region>>'),
|
||||||
|
@ -68,7 +69,6 @@ menudefs = [
|
||||||
('Untabify Region', '<<untabify-region>>'),
|
('Untabify Region', '<<untabify-region>>'),
|
||||||
('Toggle Tabs', '<<toggle-tabs>>'),
|
('Toggle Tabs', '<<toggle-tabs>>'),
|
||||||
('New Indent Width', '<<change-indentwidth>>'),
|
('New Indent Width', '<<change-indentwidth>>'),
|
||||||
('F_ormat Paragraph', '<<format-paragraph>>'),
|
|
||||||
('S_trip Trailing Whitespace', '<<do-rstrip>>'),
|
('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…
Add table
Add a link
Reference in a new issue