Repository: spyder-ide/spyder

Issue:
Completion popup menu isn't dismissed after typing a delimiter and causes unexpected behavior
<!--- **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! --->

## Problem Description

The completion popup dialog (list of possible completions) that appears with either automatic or manual completion isn't dismissed once the user types a Python delimiter (paren, bracket, space, etc).

Therefore, if the user manually types in the desired name, it stays up when they continue typing, and sometimes even continues to change the completion results depending on whatever they are typing next. Then, when they press ``Enter`` to move to the next line, it triggers completion instead, modifies the name they originally triggered completion on and moves their cursor to the end of that.

Some times, I didn't notice this happening, and as a result it broke my code by changing what I'd previously typed to something completely different, and was otherwise a quite serious nuisance to the point I would have considered turning Automatic Completions off if that were currently possible.


### What steps reproduce the problem?

Note: ``|`` is the cursor position.

1. On a new line,  type ``np.ndarray.size == test|``
2. Press ``Enter``
3. Note the result is now ``np.ndarray.take| == test``


# Proposed Solution/Expected result

Cancel the current completion attempt if any Python [operator](https://docs.python.org/3/reference/lexical_analysis.html#operators) or [delimiter](https://docs.python.org/3/reference/lexical_analysis.html#delimiters) is typed.




## Versions

* Spyder version: 4.0.0.dev0 ``386403ed52``
* Python version: 3.7.2
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 8.1

### Dependencies

```
pygments >=2.0    :  2.3.1 (OK)
qdarkstyle >=2.6.4:  2.6.5 (OK)
sphinx >=0.6.6    :  1.8.4 (OK)
pyls >=0.19.0     :  0.21.5 (OK)
nbconvert >=4.0   :  5.4.0 (OK)
pandas >=0.13.1   :  0.24.1 (OK)
numpy >=1.7       :  1.15.4 (OK)
sympy >=0.7.3     :  1.3 (OK)
cython >=0.21     :  0.29.4 (OK)
qtconsole >=4.2.0 :  4.4.3 (OK)
IPython >=4.0     :  7.2.0 (OK)
matplotlib >=2.0.0:  3.0.2 (OK)
pylint >=0.25     :  2.2.2 (OK)
```



Discussion:

Resolving patch:
diff --git a/spyder/plugins/editor/extensions/closebrackets.py b/spyder/plugins/editor/extensions/closebrackets.py
--- a/spyder/plugins/editor/extensions/closebrackets.py
+++ b/spyder/plugins/editor/extensions/closebrackets.py
@@ -37,6 +37,7 @@ def _on_key_pressed(self, event):
 
         key = event.key()
         if key in self.BRACKETS_CHAR and self.enabled:
+            self.editor.completion_widget.hide()
             self._autoinsert_brackets(key)
             event.accept()
 
diff --git a/spyder/plugins/editor/extensions/closequotes.py b/spyder/plugins/editor/extensions/closequotes.py
--- a/spyder/plugins/editor/extensions/closequotes.py
+++ b/spyder/plugins/editor/extensions/closequotes.py
@@ -54,6 +54,7 @@ def _on_key_pressed(self, event):
 
         key = event.key()
         if key in (Qt.Key_QuoteDbl, Qt.Key_Apostrophe) and self.enabled:
+            self.editor.completion_widget.hide()
             self._autoinsert_quotes(key)
             event.accept()
 
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
@@ -2600,6 +2600,7 @@ def __has_colon_not_in_brackets(self, text):
     def autoinsert_colons(self):
         """Decide if we want to autoinsert colons"""
         bracket_ext = self.editor_extensions.get(CloseBracketsExtension)
+        self.completion_widget.hide()
         line_text = self.get_text('sol', 'cursor')
         if not self.textCursor().atBlockEnd():
             return False
@@ -2775,6 +2776,12 @@ def keyPressEvent(self, event):
         has_selection = self.has_selected_text()
         ctrl = event.modifiers() & Qt.ControlModifier
         shift = event.modifiers() & Qt.ShiftModifier
+        operators = {'+', '-', '*', '**', '/', '//', '%', '@', '<<', '>>',
+                     '&', '|', '^', '~', '<', '>', '<=', '>=', '==', '!='}
+        delimiters = {',', ':', ';', '@', '=', '->', '+=', '-=', '*=', '/=',
+                      '//=', '%=', '@=', '&=', '|=', '^=', '>>=', '<<=', '**='}
+        if text in operators or text in delimiters:
+            self.completion_widget.hide()
         if key in (Qt.Key_Enter, Qt.Key_Return):
             if not shift and not ctrl:
                 if self.add_colons_enabled and self.is_python_like() and \
@@ -2869,6 +2876,7 @@ def keyPressEvent(self, event):
             TextEditBaseWidget.keyPressEvent(self, event)
         elif key == Qt.Key_Space and not shift and not ctrl \
              and not has_selection and self.auto_unindent_enabled:
+            self.completion_widget.hide()
             leading_text = self.get_text('sol', 'cursor')
             if leading_text.lstrip() in ('elif', 'except'):
                 ind = lambda txt: len(txt)-len(txt.lstrip())


Test patch:
diff --git a/spyder/plugins/editor/tests/test_introspection.py b/spyder/plugins/editor/tests/test_introspection.py
--- a/spyder/plugins/editor/tests/test_introspection.py
+++ b/spyder/plugins/editor/tests/test_introspection.py
@@ -12,6 +12,7 @@
 # Third party imports
 import pytest
 import pytestqt
+import random
 try:
     from unittest.mock import Mock
 except ImportError:
@@ -99,6 +100,41 @@ def test_space_completion(setup_editor):
     assert code_editor.toPlainText() == 'from numpy import\n'
 
 
+# @pytest.mark.slow
+def test_hide_widget_completion(setup_editor):
+    """Validate hiding completion widget after a delimeter or operator."""
+    editor, qtbot = setup_editor
+    code_editor = editor.get_focus_widget()
+    completion = code_editor.completion_widget
+
+    delimiters = ['(', ')', '[', ']', '{', '}', ',', ':', ';', '@', '=', '->',
+                  '+=', '-=', '*=', '/=', '//=', '%=', '@=', '&=', '|=', '^=',
+                  '>>=', '<<=', '**=']
+    # Set cursor to start
+    code_editor.go_to_line(1)
+
+    # Complete from numpy --> from numpy import
+    qtbot.keyClicks(code_editor, 'from numpy ')
+    qtbot.wait(20000)
+
+    # press tab and get completions
+    with qtbot.waitSignal(completion.sig_show_completions,
+                          timeout=10000) as sig:
+        qtbot.keyPress(code_editor, Qt.Key_Tab)
+
+    # Check the completion widget is visible
+    assert completion.isHidden() is False
+
+    # Write a random delimeter on the code editor
+    delimeter = random.choice(delimiters)
+    qtbot.keyClicks(code_editor, delimeter)
+
+    # Check the completion widget is not visible
+    assert completion.isHidden() is True
+
+    qtbot.keyPress(code_editor, Qt.Key_Enter)
+
+
 @pytest.mark.slow
 @pytest.mark.skipif(PY2, reason="Segfaults with other tests on Py2.")
 @pytest.mark.skipif(os.name == 'nt' and not PY2,