Repository: spyder-ide/spyder

Issue:
Errors when removing variables in the Variable Explorer
## Problem Description
In panel "Variable explorer", right click on a variable, select "remove". It deletes the variable below instead of deleting the current variable

### What steps reproduce the problem?
1. Run a python script, which returns several variable
2. Open variable explorer, right click on a variable, select "remove".

### What is the expected output? What do you see instead?
Expected: to remove the chosen variable
Instead: the variable below (on the list) was removed.

## Versions
* Spyder version: 4.0.0 rc1
* Python version: 3.7.4 (64-bit)
* Qt version: 5.9.2
* PyQt version:  5.9.6
* Operating System name/version: Windows 10




Discussion:
@dalthviz could you take a look?
This problem was mentioned in #10702 as well

Resolving patch:
diff --git a/spyder/plugins/variableexplorer/widgets/collectionseditor.py b/spyder/plugins/variableexplorer/widgets/collectionseditor.py
--- a/spyder/plugins/variableexplorer/widgets/collectionseditor.py
+++ b/spyder/plugins/variableexplorer/widgets/collectionseditor.py
@@ -518,6 +518,8 @@ def __init__(self, parent):
         self.last_regex = ''
         self.view_action = None
         self.delegate = None
+        self.proxy_model = None
+        self.source_model = None
         self.setAcceptDrops(True)
         self.automatic_column_width = True
         self.setHorizontalHeader(BaseHeaderView(parent=self))
@@ -857,7 +859,7 @@ def edit_item(self):
         self.edit(index.child(index.row(), 3))
 
     @Slot()
-    def remove_item(self):
+    def remove_item(self, force=False):
         """Remove item"""
         indexes = self.selectedIndexes()
         if not indexes:
@@ -865,13 +867,15 @@ def remove_item(self):
         for index in indexes:
             if not index.isValid():
                 return
-        one = _("Do you want to remove the selected item?")
-        more = _("Do you want to remove all selected items?")
-        answer = QMessageBox.question(self, _( "Remove"),
-                                      one if len(indexes) == 1 else more,
-                                      QMessageBox.Yes | QMessageBox.No)
-        if answer == QMessageBox.Yes:
-            idx_rows = unsorted_unique([idx.row() for idx in indexes])
+        if not force:
+            one = _("Do you want to remove the selected item?")
+            more = _("Do you want to remove all selected items?")
+            answer = QMessageBox.question(self, _("Remove"),
+                                          one if len(indexes) == 1 else more,
+                                          QMessageBox.Yes | QMessageBox.No)
+        if force or answer == QMessageBox.Yes:
+            idx_rows = unsorted_unique(
+                [self.proxy_model.mapToSource(idx).row() for idx in indexes])
             keys = [self.source_model.keys[idx_row] for idx_row in idx_rows]
             self.remove_values(keys)
 


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -2428,6 +2428,58 @@ def test_runcell(main_window, qtbot, tmpdir, debug):
         pass
 
 
+@pytest.mark.slow
+@flaky(max_runs=3)
+def test_varexp_remove(main_window, qtbot, tmpdir):
+    """
+    Test removing a variable.
+    Regression test for spyder-ide/spyder#10709
+    """
+    # ---- Setup ----
+    p = (tmpdir.mkdir(u"varexp_remove").join(u"script.py"))
+    filepath = to_text_string(p)
+    shutil.copyfile(osp.join(LOCATION, 'script.py'), filepath)
+
+    # Wait until the window is fully up
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+
+    # Load test file
+    main_window.editor.load(filepath)
+
+    # Move to the editor's first line
+    code_editor = main_window.editor.get_focus_widget()
+    code_editor.setFocus()
+    qtbot.keyClick(code_editor, Qt.Key_Home, modifier=Qt.ControlModifier)
+
+    # Get a reference to the namespace browser widget
+    nsb = main_window.variableexplorer.get_focus_widget()
+
+    # ---- Run file ----
+    with qtbot.waitSignal(shell.executed):
+        qtbot.keyClick(code_editor, Qt.Key_F5)
+
+    # Wait until all objects have appeared in the variable explorer
+    qtbot.waitUntil(lambda: nsb.editor.model.rowCount() == 4,
+                    timeout=EVAL_TIMEOUT)
+
+    # Remove one element
+    nsb.editor.setCurrentIndex(nsb.editor.model.index(1, 0))
+    nsb.editor.remove_item(force=True)
+
+    # Wait until all objects have appeared in the variable explorer
+    qtbot.waitUntil(lambda: nsb.editor.model.rowCount() == 3,
+                    timeout=EVAL_TIMEOUT)
+
+    def data(cm, i, j):
+        assert cm.rowCount() == 3
+        return cm.data(cm.index(i, j))
+    assert data(nsb.editor.model, 0, 0) == 'a'
+    assert data(nsb.editor.model, 1, 0) == 'li'
+    assert data(nsb.editor.model, 2, 0) == 's'
+
+
 @pytest.mark.slow
 @flaky(max_runs=3)
 def test_varexp_refresh(main_window, qtbot):
diff --git a/spyder/plugins/variableexplorer/widgets/tests/test_collectioneditor.py b/spyder/plugins/variableexplorer/widgets/tests/test_collectioneditor.py
--- a/spyder/plugins/variableexplorer/widgets/tests/test_collectioneditor.py
+++ b/spyder/plugins/variableexplorer/widgets/tests/test_collectioneditor.py
@@ -31,8 +31,8 @@
 
 # Local imports
 from spyder.plugins.variableexplorer.widgets.collectionseditor import (
-    CollectionsEditorTableView, CollectionsModel, CollectionsEditor,
-    LARGE_NROWS, ROWS_TO_LOAD)
+    RemoteCollectionsEditorTableView, CollectionsEditorTableView,
+    CollectionsModel, CollectionsEditor, LARGE_NROWS, ROWS_TO_LOAD)
 from spyder.plugins.variableexplorer.widgets.namespacebrowser import (
     NamespacesBrowserFinder)
 from spyder.plugins.variableexplorer.widgets.tests.test_dataframeeditor import \
@@ -72,6 +72,104 @@ def nonsettable_objects_data():
 # =============================================================================
 # Tests
 # ============================================================================
+def test_remove_variable(qtbot):
+    """Test removing of the correct variable."""
+    variables = {'a': 1,
+                 'b': 2,
+                 'c': 3,
+                 'd': '4',
+                 'e': 5}
+    editor = CollectionsEditorTableView(None, variables.copy())
+    qtbot.addWidget(editor)
+    editor.setCurrentIndex(editor.model.index(1, 0))
+
+    editor.remove_item(force=True)
+    assert editor.model.rowCount() == 4
+    assert data(editor.model, 0, 0) == 'a'
+    assert data(editor.model, 1, 0) == 'c'
+    assert data(editor.model, 2, 0) == 'd'
+    assert data(editor.model, 3, 0) == 'e'
+
+    # Reset variables and try removing one again
+    editor.set_data(variables.copy())
+    editor.adjust_columns()
+    editor.setCurrentIndex(editor.model.index(1, 0))
+    editor.remove_item(force=True)
+    assert editor.model.rowCount() == 4
+    assert data(editor.model, 0, 0) == 'a'
+    assert data(editor.model, 1, 0) == 'c'
+    assert data(editor.model, 2, 0) == 'd'
+    assert data(editor.model, 3, 0) == 'e'
+
+
+def test_remove_remote_variable(qtbot, monkeypatch):
+    """Test the removing of the correct remote variable."""
+    variables = {'a': {'type': 'int',
+                       'size': 1,
+                       'color': '#0000ff',
+                       'view': '1'},
+                 'b': {'type': 'int',
+                       'size': 1,
+                       'color': '#0000ff',
+                       'view': '2'},
+                 'c': {'type': 'int',
+                       'size': 1,
+                       'color': '#0000ff',
+                       'view': '3'},
+                 'd': {'type': 'str',
+                       'size': 1, 'color': '#800000',
+                       'view': '4'},
+                 'e': {'type': 'int',
+                       'size': 1,
+                       'color': '#0000ff',
+                       'view': '5'}}
+    editor = RemoteCollectionsEditorTableView(None, variables.copy())
+    qtbot.addWidget(editor)
+    editor.setCurrentIndex(editor.model.index(1, 0))
+
+    # Monkey patch remove variables
+    def remove_values(ins, names):
+        assert names == ['b']
+        data = {'a': {'type': 'int',
+                      'size': 1,
+                      'color': '#0000ff',
+                      'view': '1'},
+                'c': {'type': 'int',
+                      'size': 1,
+                      'color': '#0000ff',
+                      'view': '3'},
+                'd': {'type': 'str',
+                      'size': 1, 'color': '#800000',
+                      'view': '4'},
+                'e': {'type': 'int',
+                      'size': 1,
+                      'color': '#0000ff',
+                      'view': '5'}}
+        editor.set_data(data)
+    monkeypatch.setattr(
+        'spyder.plugins.variableexplorer.widgets'
+        '.collectionseditor.RemoteCollectionsEditorTableView.remove_values',
+        remove_values)
+
+    editor.remove_item(force=True)
+    assert editor.model.rowCount() == 4
+    assert data(editor.model, 0, 0) == 'a'
+    assert data(editor.model, 1, 0) == 'c'
+    assert data(editor.model, 2, 0) == 'd'
+    assert data(editor.model, 3, 0) == 'e'
+
+    # Reset variables and try removing one again
+    editor.set_data(variables.copy())
+    editor.adjust_columns()
+    editor.setCurrentIndex(editor.model.index(1, 0))
+    editor.remove_item(force=True)
+    assert editor.model.rowCount() == 4
+    assert data(editor.model, 0, 0) == 'a'
+    assert data(editor.model, 1, 0) == 'c'
+    assert data(editor.model, 2, 0) == 'd'
+    assert data(editor.model, 3, 0) == 'e'
+
+
 def test_filter_rows(qtbot):
     """Test rows filtering."""