Repository: spyder-ide/spyder

Issue:
Hide calltip when a matching `)` is written up to the starting point where it's shown
Right now if you write

    li = [1, 2, 3]
    (max(<1>li)<2> + 6)<3>

a calltip is shown when the cursor is in `<1>`, but it's not closed when you write `)` in `<2>` but instead is closed in `<3>` when that last `)` matches the first `(` in the line.

@dalthviz, please fix this.


Discussion:

Resolving patch:
diff --git a/spyder/widgets/calltip.py b/spyder/widgets/calltip.py
--- a/spyder/widgets/calltip.py
+++ b/spyder/widgets/calltip.py
@@ -65,12 +65,17 @@ def eventFilter(self, obj, event):
 
             if etype == QEvent.KeyPress:
                 key = event.key()
+                cursor = self._text_edit.textCursor()
+                prev_char = self._text_edit.get_character(cursor.position(),
+                                                          offset=-1)
                 if key in (Qt.Key_Enter, Qt.Key_Return,
                            Qt.Key_Down, Qt.Key_Up):
                     self.hide()
                 elif key == Qt.Key_Escape:
                     self.hide()
                     return True
+                elif prev_char == ')':
+                    self.hide()
 
             elif etype == QEvent.FocusOut:
                 self.hide()
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -302,9 +302,9 @@ def get_text(self, position_from, position_to):
                 text = text[:-1]
         return text
     
-    def get_character(self, position):
-        """Return character at *position*"""
-        position = self.get_position(position)
+    def get_character(self, position, offset=0):
+        """Return character at *position* with the given offset."""
+        position = self.get_position(position) + offset
         cursor = self.textCursor()
         cursor.movePosition(QTextCursor.End)
         if position < cursor.position():


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
@@ -24,7 +24,7 @@
 
 from spyder.app.cli_options import get_options
 from spyder.app.mainwindow import initialize, run_spyder
-
+from spyder.utils.test import close_save_message_box
 
 #==============================================================================
 # Constants
@@ -81,6 +81,37 @@ def close_widget():
 #==============================================================================
 # Tests
 #==============================================================================
+@flaky(max_runs=10)
+@pytest.mark.skipif(os.name != 'nt' and PYQT5,
+                    reason="It times out sometimes on Linux with PyQt5")
+def test_calltip(main_window, qtbot):
+    """Hide the calltip in the editor when a matching ')' is found."""
+    # Load test file
+    text = 'a = [1,2,3]\n(max'
+    main_window.editor.new(fname="test.py", text=text)
+    code_editor = main_window.editor.get_focus_widget()
+    
+    # Set text to start
+    code_editor.set_text(text)
+    code_editor.go_to_line(2)
+    code_editor.move_cursor(5)
+    calltip = code_editor.calltip_widget
+    assert not calltip.isVisible()
+
+    qtbot.keyPress(code_editor, Qt.Key_ParenLeft, delay=3000)
+    qtbot.keyPress(code_editor, Qt.Key_A, delay=1000)
+    qtbot.waitUntil(lambda: calltip.isVisible(), timeout=1000)
+
+    qtbot.keyPress(code_editor, Qt.Key_ParenRight, delay=1000)
+    qtbot.keyPress(code_editor, Qt.Key_Space)
+    assert not calltip.isVisible()
+    qtbot.keyPress(code_editor, Qt.Key_ParenRight, delay=1000)
+    qtbot.keyPress(code_editor, Qt.Key_Enter, delay=1000)
+        
+    QTimer.singleShot(1000, lambda: close_save_message_box(qtbot))
+    main_window.editor.close_file()
+
+
 @flaky(max_runs=10)
 @pytest.mark.skipif(os.name == 'nt', reason="It times out sometimes on Windows")
 def test_open_notebooks_from_project_explorer(main_window, qtbot):
diff --git a/spyder/utils/test.py b/spyder/utils/test.py
--- a/spyder/utils/test.py
+++ b/spyder/utils/test.py
@@ -6,6 +6,9 @@
 
 """Tests utilities."""
 
+# Standard library imports
+import os
+
 # Third party imports
 from qtpy.QtCore import Qt
 from qtpy.QtWidgets import QMessageBox, QApplication
@@ -22,3 +25,19 @@ def close_message_box(qtbot):
     for w in top_level_widgets:
         if isinstance(w, QMessageBox):
             qtbot.keyClick(w, Qt.Key_Enter)
+
+def close_save_message_box(qtbot):
+    """
+    Closes QMessageBox's for save that can appear when testing.
+
+    You can use this with QTimer to close a QMessageBox for save functions.
+    Before calling anything that may show a QMessageBox for save call:
+    QTimer.singleShot(1000, lambda: close_save_message_box(qtbot))
+    """
+    top_level_widgets = QApplication.topLevelWidgets()
+    for w in top_level_widgets:
+        if isinstance(w, QMessageBox):
+            if os.name == 'nt':
+               qtbot.keyClick(w, Qt.Key_Enter)
+            else:
+               qtbot.keyClick(w, Qt.Key_N, modifier=Qt.ShiftModifier)