mirror of
https://github.com/python/cpython.git
synced 2026-10-28 18:35:21 +08:00
gh-71956: Fix IDLE Replace All searching up without wrap around (#152737)
When the search direction is "Up" and "Wrap around" is off, Replace All replaced only the first match above the current position (and all matches below it). It now replaces all matches from the start of the text down to the current position, consistently with the "Up" direction.
This commit is contained in:
1 parent
4e16b8d4ee
commit
ef27e5b310
3 files changed
+105
-13
No files matched your search
@@ -246,32 +246,108 @@ class ReplaceDialogTest(unittest.TestCase):
|
||||
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
|
||||
|
||||
+21
-8
@@ -122,12 +122,13 @@ class ReplaceDialog(SearchDialogBase):
|
||||
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:
|
||||
@@ -142,9 +143,18 @@ class ReplaceDialog(SearchDialogBase):
|
||||
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
|
||||
@@ -152,12 +162,13 @@ class ReplaceDialog(SearchDialogBase):
|
||||
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:
|
||||
@@ -170,6 +181,8 @@ class ReplaceDialog(SearchDialogBase):
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user