Repository: spyder-ide/spyder

Issue:
Uncommenting adds an extra whitespace in some cases
### 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
* [x] 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

Commenting and uncommenting a line with an odd number of leading whitespace adds an extra space.

### What steps reproduce the problem?

1. Type
```
x = [1,
     2,
     3]
```
2. Select all 3 lines, comment and uncomment
3. The code is now
```
x = [1,
      2,
      3]
```

### What is the expected output? What do you see instead?

The code after step 3 should be
```
x = [1,
     2,
     3]
```

If the number of leading whitespace is even, for ex:
```
xx = [1,
      2,
      3]

```
then there is no issue. 

## Versions

* Spyder version: 4.2.0
* Python version: 3.8.5
* Qt version: 5.9.7
* PyQt version: 5.9.2
* Operating System name/version: Linux 5.9.10

### Dependencies

```
# Mandatory:
atomicwrites >=1.2.0            :  1.4.0 (OK)
chardet >=2.0.0                 :  3.0.4 (OK)
cloudpickle >=0.5.0             :  1.6.0 (OK)
diff_match_patch >=20181111     :  20200713 (OK)
intervaltree >=3.0.2            :  3.0.2 (OK)
IPython >=4.0                   :  7.19.0 (OK)
jedi =0.17.2                    :  0.17.2 (OK)
jsonschema >=3.2.0              :  3.2.0 (OK)
keyring >=17.0.0                :  21.5.0 (OK)
nbconvert >=4.0                 :  6.0.7 (OK)
numpydoc >=0.6.0                :  1.1.0 (OK)
parso =0.7.0                    :  0.7.0 (OK)
pexpect >=4.4.0                 :  4.8.0 (OK)
pickleshare >=0.4               :  0.7.5 (OK)
psutil >=5.3                    :  5.7.3 (OK)
pygments >=2.0                  :  2.7.2 (OK)
pylint >=1.0                    :  2.6.0 (OK)
pyls >=0.36.1;<1.0.0            :  0.36.1 (OK)
pyls_black >=0.4.6              :  0.4.6 (OK)
pyls_spyder >=0.1.1             :  0.2.1 (OK)
qdarkstyle >=2.8                :  2.8.1 (OK)
qtawesome >=0.5.7               :  1.0.1 (OK)
qtconsole >=4.7.7               :  5.0.1 (OK)
qtpy >=1.5.0                    :  1.9.0 (OK)
rtree >=0.8.3                   :  0.9.4 (OK)
setuptools >=39.0.0             :  50.3.1.post20201107 (OK)
sphinx >=0.6.6                  :  3.3.1 (OK)
spyder_kernels >=1.10.0;<1.11.0 :  1.10.0 (OK)
three_merge >=0.1.1             :  0.1.1 (OK)
watchdog >=0.10.3               :  0.10.4 (OK)
xdg >=0.26                      :  0.26 (OK)
zmq >=17                        :  20.0.0 (OK)

# Optional:
cython >=0.21                   :  None (NOK)
matplotlib >=2.0.0              :  None (NOK)
numpy >=1.7                     :  None (NOK)
pandas >=1.1.1                  :  None (NOK)
scipy >=0.17.0                  :  None (NOK)
sympy >=0.7.3                   :  None (NOK)
```



Discussion:
Hi @remisalmon,

I could reproduce this issue, we will fix it for our future version of spyder.

Thanks for reporting!
Is there any chance for a fix soon? This bug persists in v5.0.0.
@Mark531 I have opened at PR at https://github.com/spyder-ide/spyder/pull/14768.
Same here, this can be reproduced with tabs instead of whitespace as indent type. *Toggle Comment* adds a whitespace and when the line is indented, the space remains. 
Using Spyder 5.3.3

Resolving patch:
diff --git a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py
--- a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py
@@ -2374,43 +2374,20 @@ def remove_prefix(self, prefix):
 
     def __remove_prefix(self, prefix, cursor, line_text):
         """Handle the removal of the prefix for a single line."""
-        start_with_space = line_text.startswith(' ')
-        if start_with_space:
-            left_spaces = self.__even_number_of_spaces(line_text)
-        else:
-            left_spaces = False
-        if start_with_space:
-            right_number_spaces = self.__number_of_spaces(line_text, group=1)
-        else:
-            right_number_spaces = self.__number_of_spaces(line_text)
+        cursor.movePosition(QTextCursor.Right,
+                            QTextCursor.MoveAnchor,
+                            line_text.find(prefix))
         # Handle prefix remove for comments with spaces
         if (prefix.strip() and line_text.lstrip().startswith(prefix + ' ')
                 or line_text.startswith(prefix + ' ') and '#' in prefix):
             cursor.movePosition(QTextCursor.Right,
-                                QTextCursor.MoveAnchor,
-                                line_text.find(prefix))
-            if (right_number_spaces == 1
-                    and (left_spaces or not start_with_space)
-                    or (not start_with_space and right_number_spaces % 2 != 0)
-                    or (left_spaces and right_number_spaces % 2 != 0)):
-                # Handle inserted '# ' with the count of the number of spaces
-                # at the right and left of the prefix.
-                cursor.movePosition(QTextCursor.Right,
-                                    QTextCursor.KeepAnchor, len(prefix + ' '))
-            else:
-                # Handle manual insertion of '#'
-                cursor.movePosition(QTextCursor.Right,
-                                    QTextCursor.KeepAnchor, len(prefix))
-            cursor.removeSelectedText()
+                                QTextCursor.KeepAnchor, len(prefix + ' '))
         # Check for prefix without space
         elif (prefix.strip() and line_text.lstrip().startswith(prefix)
                 or line_text.startswith(prefix)):
-            cursor.movePosition(QTextCursor.Right,
-                                QTextCursor.MoveAnchor,
-                                line_text.find(prefix))
             cursor.movePosition(QTextCursor.Right,
                                 QTextCursor.KeepAnchor, len(prefix))
-            cursor.removeSelectedText()
+        cursor.removeSelectedText()
 
     def __even_number_of_spaces(self, line_text, group=0):
         """


Test patch:
diff --git a/spyder/plugins/editor/widgets/codeeditor/tests/test_comments.py b/spyder/plugins/editor/widgets/codeeditor/tests/test_comments.py
--- a/spyder/plugins/editor/widgets/codeeditor/tests/test_comments.py
+++ b/spyder/plugins/editor/widgets/codeeditor/tests/test_comments.py
@@ -62,21 +62,21 @@ def test_single_line_comment(codeeditor):
     # Toggle comment with space at the right of prefix but manually inserted
     text = toggle_comment(editor, start_line=2)
     assert text == ("class a():\n"
-                    "    self.b = 1\n"
+                    "   self.b = 1\n"
                     " #   print(self.b)\n"
                     "#    \n"
                     )
     # Toggle comment with space insertion
     text = toggle_comment(editor, start_line=2)
     assert text == ("class a():\n"
-                    "    # self.b = 1\n"
+                    "   # self.b = 1\n"
                     " #   print(self.b)\n"
                     "#    \n"
                     )
     # Toggle comment deleting inserted space
     text = toggle_comment(editor, start_line=2)
     assert text == ("class a():\n"
-                    "    self.b = 1\n"
+                    "   self.b = 1\n"
                     " #   print(self.b)\n"
                     "#    \n"
                     )
@@ -84,8 +84,8 @@ def test_single_line_comment(codeeditor):
     # but manually inserted
     text = toggle_comment(editor, start_line=3)
     assert text == ("class a():\n"
-                    "    self.b = 1\n"
-                    "    print(self.b)\n"
+                    "   self.b = 1\n"
+                    "   print(self.b)\n"
                     "#    \n"
                     )
 
@@ -102,23 +102,23 @@ def test_selection_comment(codeeditor):
     # Toggle manually commented code
     text = toggle_comment(editor, single_line=False)
     assert text == ("class a():\n"
-                    "    self.b = 1\n"
-                    "    print(self.b)\n"
-                    "    \n"
+                    "   self.b = 1\n"
+                    "   print(self.b)\n"
+                    "   \n"
                     )
     # Toggle comment inserting prefix and space
     text = toggle_comment(editor, single_line=False)
     assert text == ("# class a():\n"
-                    "#     self.b = 1\n"
-                    "#     print(self.b)\n"
-                    "    \n"
+                    "#    self.b = 1\n"
+                    "#    print(self.b)\n"
+                    "   \n"
                     )
     # Toggle comment deleting inserted prefix and space
     text = toggle_comment(editor, single_line=False)
     assert text == ("class a():\n"
-                    "    self.b = 1\n"
-                    "    print(self.b)\n"
-                    "    \n"
+                    "   self.b = 1\n"
+                    "   print(self.b)\n"
+                    "   \n"
                     )
     # Test compatibility with Spyder 3 commenting structure
     text = ("#class a():\n"
@@ -130,9 +130,9 @@ def test_selection_comment(codeeditor):
     # Toggle comment deleting inserted prefix (without space)
     text = toggle_comment(editor, single_line=False)
     assert text == ("class a():\n"
-                    "    self.b = 1\n"
-                    "    print(self.b)\n"
-                    "    \n"
+                    "   self.b = 1\n"
+                    "   print(self.b)\n"
+                    "   \n"
                     )