Repository: spyder-ide/spyder

Issue:
Auto-format sometimes jumps to a different position if Editor > Display > Wrap lines is set
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

* [x] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [x] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [ ] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [ ] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [x] Tried basic troubleshooting (if a bug/error)
    * [x] Restarted Spyder
    * [x] Reset preferences with ``spyder --reset``
    * [ ] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [x] Tried the other applicable steps from the Troubleshooting Guide
* [x] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below

## Problem Description

One of our users has reported that auto-format with Black still sometimes jumps to a different place in the file.  It's not as dramatic as in #19958 (closed), but it's still clearly unideal.

The full report, including a video showing the behaviour and a Python script exhibiting the behaviour are attached: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1034085

Below I describe the minimum needed to trigger this bug with this Python script.  I am working with Spyder 5.4.2 on Linux (Debian is currently in freeze, so I can't update Spyder for a few months) and Spyder 5.4.3 on a Mac, and they both exhibit this behaviour.

### What steps reproduce the problem?

1. Reset spyder preferences
2. Start spyder
3. In preferences, select Editor > Display > Wrap lines
4. Open practice.py (attached to this issue as practice.py.txt)
[practice.py.txt](https://github.com/spyder-ide/spyder/files/11276943/practice.py.txt)
5. Go to line 270 and remove a space around the `//` operator
6. Right-click and select "Format file or selection with Autopep8"

### What is the expected output? What do you see instead?

The cursor jumps back to line 264.  The same happens with Black.  The cursor shouldn't jump.

On the other hand, if I have a shorter file with only the `sort_nums` function, it doesn't exhibit this undesired behaviour.

### Paste Traceback/Error Below (if applicable)

See the Debian bug report for a screencast.

## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 5.4.2 (Debian, used for the rest of the info below)
* Python version: 3.11.2 64-bit
* Qt version: 5.15.8
* PyQt5 version: 5.15.9
* Operating System: Linux 6.1.0-7-amd64

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
# Mandatory:
atomicwrites >=1.2.0                     :  1.4.1 (OK)
chardet >=2.0.0                          :  5.1.0 (OK)
cloudpickle >=0.5.0                      :  2.2.0 (OK)
cookiecutter >=1.6.0                     :  1.7.3 (OK)
diff_match_patch >=20181111              :  20200713 (OK)
intervaltree >=3.0.2                     :  3.0.2 (OK)
IPython >=7.31.1;<9.0.0                  :  8.5.0 (OK)
jedi >=0.17.2;<0.19.0                    :  0.18.2 (OK)
jellyfish >=0.7                          :  0.8.9 (OK)
jsonschema >=3.2.0                       :  4.10.3 (OK)
keyring >=17.0.0                         :  23.9.3 (OK)
nbconvert >=4.0                          :  6.5.3 (OK)
numpydoc >=0.6.0                         :  1.5.0 (OK)
parso >=0.7.0;<0.9.0                     :  0.8.3 (OK)
pexpect >=4.4.0                          :  4.8.0 (OK)
pickleshare >=0.4                        :  0.7.5 (OK)
psutil >=5.3                             :  5.9.4 (OK)
pygments >=2.0                           :  2.14.0 (OK)
pylint >=2.5.0;<3.0                      :  2.16.2 (OK)
pylint_venv >=2.1.1                      :  2.3.0 (OK)
pyls_spyder >=0.4.0                      :  0.4.0 (OK)
pylsp >=1.7.1;<1.8.0                     :  1.7.1 (OK)
pylsp_black >=1.2.0                      :  1.2.1 (OK)
qdarkstyle >=3.0.2;<3.2.0                :  3.1 (OK)
qstylizer >=0.2.2                        :  0.2.2 (OK)
qtawesome >=1.2.1                        :  1.2.2 (OK)
qtconsole >=5.4.0;<5.5.0                 :  5.4.0 (OK)
qtpy >=2.1.0                             :  2.3.0 (OK)
rtree >=0.9.7                            :  1.0.1 (OK)
setuptools >=49.6.0                      :  66.1.1 (OK)
sphinx >=0.6.6                           :  5.3.0 (OK)
spyder_kernels >=2.4.2;<2.5.0            :  2.4.2 (OK)
textdistance >=4.2.0                     :  4.5.0 (OK)
three_merge >=0.1.1                      :  0.1.1 (OK)
watchdog >=0.10.3                        :  2.2.1 (OK)
xdg >=0.26                               :  0.28 (OK)
zmq >=22.1.0                             :  24.0.1 (OK)

# Optional:
cython >=0.21                            :  0.29.32 (OK)
matplotlib >=3.0.0                       :  3.6.3 (OK)
numpy >=1.7                              :  1.24.2 (OK)
pandas >=1.1.1                           :  1.5.3 (OK)
scipy >=0.17.0                           :  1.10.1 (OK)
sympy >=0.7.3                            :  1.11.1 (OK)

# Spyder plugins:
spyder_line_profiler.spyder.plugin 0.3.1 :  0.3.1 (OK)
spyder_unittest.unittestplugin 0.5.1     :  0.5.1 (OK)
```



Discussion:
Hi @juliangilbey thanks for the feedback and the detailed explanation! I was able to reproduce this. I think the wrap lines functionality has an issue open so my guess is that this could be related with that one (https://github.com/spyder-ide/spyder/issues/20802). We will try to fix this in a future release :+1:


Resolving patch:
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -526,7 +526,7 @@ def __init__(self, parent=None):
         self.format_on_save = False
         self.format_eventloop = QEventLoop(None)
         self.format_timer = QTimer(self)
-        self.__line_number_before_format = 0
+        self.__cursor_position_before_format = 0
 
         # Mouse tracking
         self.setMouseTracking(True)
@@ -1808,7 +1808,7 @@ def format_document_or_range(self):
     @schedule_request(method=CompletionRequestTypes.DOCUMENT_FORMATTING)
     def format_document(self):
         """Format current document."""
-        self.__line_number_before_format = self.textCursor().blockNumber()
+        self.__cursor_position_before_format = self.textCursor().position()
 
         if not self.formatting_enabled:
             return
@@ -1843,7 +1843,7 @@ def format_document(self):
     @schedule_request(method=CompletionRequestTypes.DOCUMENT_RANGE_FORMATTING)
     def format_document_range(self):
         """Format selected text."""
-        self.__line_number_before_format = self.textCursor().blockNumber()
+        self.__cursor_position_before_format = self.textCursor().position()
 
         if not self.range_formatting_enabled or not self.has_selected_text():
             return
@@ -2008,14 +2008,13 @@ def _apply_document_edits(self, edits):
 
             # Restore previous cursor line and center it.
             # Fixes spyder-ide/spyder#19958
-            if self.__line_number_before_format < self.blockCount():
+            # Use QTextCursor.(position | setPosition) to restore the cursor
+            # position to be able to do it with any wrap mode
+            # Fixes spyder-ide/spyder#20852
+            if self.__cursor_position_before_format:
                 self.moveCursor(QTextCursor.Start)
                 cursor = self.textCursor()
-                cursor.movePosition(
-                    QTextCursor.Down,
-                    QTextCursor.MoveAnchor,
-                    self.__line_number_before_format
-                )
+                cursor.setPosition(self.__cursor_position_before_format)
                 self.setTextCursor(cursor)
                 self.centerCursor()
 


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_formatting.py b/spyder/plugins/editor/widgets/tests/test_formatting.py
--- a/spyder/plugins/editor/widgets/tests/test_formatting.py
+++ b/spyder/plugins/editor/widgets/tests/test_formatting.py
@@ -302,6 +302,7 @@ def test_formatting_on_save(completions_editor, formatter, qtbot):
     # Make a simple change to the file
     code_editor.moveCursor(QTextCursor.EndOfLine)
     qtbot.keyPress(code_editor, Qt.Key_Space)
+    current_position = cursor.position()
     qtbot.wait(500)
 
     # Save the file
@@ -313,4 +314,4 @@ def test_formatting_on_save(completions_editor, formatter, qtbot):
     # Check that auto-formatting was applied on save and that we restored the
     # previous line.
     assert code_editor.get_text_with_eol() == expected
-    assert code_editor.textCursor().blockNumber() == current_line
+    assert code_editor.textCursor().position() == current_position