Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 81 additions & 5 deletions Lib/idlelib/idle_test/test_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,32 +246,108 @@ def test_replace_backwards(self):
equal(text.get('1.2', '1.5'), 'was')

def test_replace_all(self):
# The default mode, forward with wrap around, replaces every
# match, both below and above the current position.
equal = self.assertEqual
text = self.text
pv = self.engine.patvar
rv = self.dialog.replvar
replace_all = self.dialog.replace_all

text.insert('insert', '\n')
text.insert('insert', text.get('1.0', 'end')*100)
pv.set('is')
rv.set('was')
text.delete('1.0', 'end')
text.insert('1.0', 'a\na\na\n')
text.mark_set('insert', '2.1')
pv.set('a')
rv.set('b')
replace_all()
self.assertNotIn('is', text.get('1.0', 'end'))
equal(text.get('1.0', '3.end'), 'b\nb\nb') # Wrapped around.

# An empty regular expression is reported as an error.
self.engine.revar.set(True)
pv.set('')
replace_all()
self.assertIn('error', showerror.title)
self.assertIn('Empty', showerror.message)

# An invalid replacement expression is reported as an error,
# and nothing is replaced.
text.delete('1.0', 'end')
text.insert('1.0', 'asT')
pv.set('[s][T]')
rv.set('\\')
replace_all()
self.assertIn('error', showerror.title)
self.assertIn('Invalid Replace Expression', showerror.message)
equal(text.get('1.0', '1.end'), 'asT')

# A pattern that is not present replaces nothing.
self.engine.revar.set(False)
text.delete('1.0', 'end')
text.insert('1.0', 'unchanged')
pv.set('text which is not present')
rv.set('foobar')
replace_all()
equal(text.get('1.0', '1.end'), 'unchanged')

def test_replace_all_backwards_no_wrap(self):
# gh-71956: 'up' without wrap replaces all matches from the start
# of the text down to the current position, not just one up.
equal = self.assertEqual
text = self.text
pv = self.engine.patvar
rv = self.dialog.replvar
replace_all = self.dialog.replace_all
self.engine.backvar.set(True)
self.engine.wrapvar.set(False)

text.delete('1.0', 'end')
text.insert('1.0', 'a\na\na\n')
text.mark_set('insert', '2.1')
pv.set('a')
rv.set('b')
replace_all()
equal(text.get('1.0', '1.end'), 'b') # Above the cursor.
equal(text.get('2.0', '2.end'), 'b') # At the cursor.
equal(text.get('3.0', '3.end'), 'a') # Below the cursor, untouched.

def test_replace_all_forwards_no_wrap(self):
# 'down' without wrap replaces all matches from the current
# position to the end of the text, and none before it.
equal = self.assertEqual
text = self.text
pv = self.engine.patvar
rv = self.dialog.replvar
replace_all = self.dialog.replace_all
self.engine.backvar.set(False)
self.engine.wrapvar.set(False)

text.delete('1.0', 'end')
text.insert('1.0', 'a\na\na\n')
text.mark_set('insert', '2.1')
pv.set('a')
rv.set('b')
replace_all()
equal(text.get('1.0', '1.end'), 'a') # Before the cursor, untouched.
equal(text.get('2.0', '2.end'), 'a') # Before the cursor, untouched.
equal(text.get('3.0', '3.end'), 'b') # After the cursor.

def test_replace_all_backwards_wrap(self):
# With wrap around, an 'up' search also replaces every match.
equal = self.assertEqual
text = self.text
pv = self.engine.patvar
rv = self.dialog.replvar
replace_all = self.dialog.replace_all
self.engine.backvar.set(True)
self.engine.wrapvar.set(True)

text.delete('1.0', 'end')
text.insert('1.0', 'a\na\na\n')
text.mark_set('insert', '2.1')
pv.set('a')
rv.set('b')
replace_all()
equal(text.get('1.0', '3.end'), 'b\nb\nb')

def test_default_command(self):
text = self.text
Expand Down
29 changes: 21 additions & 8 deletions Lib/idlelib/replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,13 @@ def _replace_expand(self, m, repl):
def replace_all(self, event=None):
"""Handle the Replace All button.

Search text for occurrences of the Find value and replace
each of them. The 'wrap around' value controls the start
point for searching. If wrap isn't set, then the searching
starts at the first occurrence after the current selection;
if wrap is set, the replacement starts at the first line.
The replacement is always done top-to-bottom in the text.
Search text for occurrences of the Find value and replace each
of them. The 'wrap around' and direction values control which
occurrences are replaced. With wrap around, every occurrence is
replaced. Without it, a forward search replaces occurrences from
the current position to the end of the text, and a backward search
replaces occurrences from the beginning of the text to the current
position. The replacement is always done top-to-bottom.
"""
prog = self.engine.getprog()
if not prog:
Expand All @@ -142,22 +143,32 @@ def replace_all(self, event=None):
text.tag_remove("hit", "1.0", "end")
line = res[0]
col = res[1].start()
# For a backward search without wrap, replace top-to-bottom from
# the start of the text down to the first match at or above the
# current position (gh-71956). A mark tracks that stop point.
stop = None
if self.engine.iswrap():
line = 1
col = 0
elif self.engine.isback():
stop = "replace_all_stop"
text.mark_set(stop, "%d.%d" % (line, res[1].end()))
line = 1
col = 0
ok = True
first = last = None
# XXX ought to replace circular instead of top-to-bottom when wrapping
text.undo_block_start()
while res := self.engine.search_forward(
text, prog, line, col, wrap=False, ok=ok):
line, m = res
chars = text.get("%d.0" % line, "%d.0" % (line+1))
i, j = m.span()
if stop is not None and text.compare("%d.%d" % (line, i), ">=", stop):
break
orig = m.group()
new = self._replace_expand(m, repl)
if new is None:
break
i, j = m.span()
first = "%d.%d" % (line, i)
last = "%d.%d" % (line, j)
if new == orig:
Expand All @@ -170,6 +181,8 @@ def replace_all(self, event=None):
text.insert(first, new, self.insert_tags)
col = i + len(new)
ok = False
if stop is not None:
text.mark_unset(stop)
text.undo_block_stop()
if first and last:
self.show_hit(first, last)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix Replace All in the IDLE editor's Replace dialog when the search
direction is "Up" and "Wrap around" is off: it now replaces all matches
above the current position instead of only the first one.
Loading