Commit 82494aa6 authored by Cheryl Sabella's avatar Cheryl Sabella Committed by Terry Jan Reedy

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.
parent fb26504d
...@@ -29,7 +29,7 @@ from idlelib.textview import view_text ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
......
"""Format a paragraph, comment block, or selection to a max width. """Format all or a selected region (line slice) of text.
Does basic, standard text formatting, and also understands Python Region formatting options: paragraph, comment block, indent, deindent,
comment blocks. Thus, for editing Python source code, this comment, uncomment, tabify, and untabify.
extension is really only suitable for reformatting these comment
blocks or triple-quoted strings. File renamed from paragraph.py with functions added from editor.py.
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 import re
from tkinter.simpledialog import askinteger
from idlelib.config import idleConf from idlelib.config import idleConf
class FormatParagraph: 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): def __init__(self, editwin):
self.editwin = editwin self.editwin = editwin
...@@ -189,6 +195,163 @@ def get_comment_header(line): ...@@ -189,6 +195,163 @@ def get_comment_header(line):
return m.group(1) 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__": if __name__ == "__main__":
from unittest import main from unittest import main
main('idlelib.idle_test.test_paragraph', verbosity=2, exit=False) main('idlelib.idle_test.test_format', verbosity=2, exit=False)
...@@ -60,6 +60,7 @@ menudefs = [ ...@@ -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 = [ ...@@ -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>>'),
]), ]),
......
Rename paragraph.py to format.py and add region formatting methods
from editor.py. Add tests for the latter.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment