Repository: spyder-ide/spyder

Issue:
Extra closing parenthesis added after new line
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

## Versions 
Python 3.7.4
Spyder 4.0.0

## Issue
Automatic insertion of parentheses, braces and brackets: ON

An extra closing parenthesis is added when typing over the auto-added parenthesis.  This happens when working inside a previous bracket.

## Reproduce

```python
import pandas as pd

df = pd.DataFrame({
    'a': [1, 2, 3],
    'b': [4, 5, 6]
    })

def paren_example():
    return( # Hit return
        df['a'].max())) # Extra parenthesis added when typing .max()
```


## Versions

* Spyder version: 4.0.0 
* Python version: 3.7.4
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 10

### Dependencies

```
cloudpickle >=0.5.0          :  1.2.2 (OK)
pygments >=2.0               :  2.5.2 (OK)
qtconsole >=4.6.0            :  4.6.0 (OK)
nbconvert >=4.0              :  5.6.1 (OK)
sphinx >=0.6.6               :  2.3.0 (OK)
pylint >=0.25                :  2.4.4 (OK)
psutil >=0.3                 :  5.6.7 (OK)
qtawesome >=0.5.7            :  0.6.0 (OK)
qtpy >=1.5.0                 :  1.9.0 (OK)
pickleshare >=0.4            :  0.7.5 (OK)
zmq >=17                     :  18.1.0 (OK)
chardet >=2.0.0              :  3.0.4 (OK)
numpydoc >=0.6.0             :  0.9.1 (OK)
spyder_kernels >=1.8.1;<2.0.0:  1.8.1 (OK)
qdarkstyle >=2.7             :  2.7 (OK)
atomicwrites >=1.2.0         :  1.3.0 (OK)
diff_match_patch >=20181111  :  20181111 (OK)
intervaltree                 :  None (OK)
watchdog                     :  None (OK)
keyring                      :  None (OK)
pexpect >=4.4.0              :  4.7.0 (OK)
pympler                      :  None (OK)
sympy >=0.7.3                :  1.5 (OK)
cython >=0.21                :  0.29.14 (OK)
IPython >=4.0                :  7.10.2 (OK)
matplotlib >=2.0.0           :  3.1.1 (OK)
pandas >=0.13.1              :  0.25.3 (OK)
numpy >=1.7                  :  1.17.4 (OK)
scipy >=0.17.0               :  1.3.2 (OK)
pyls >=0.31.2;<0.32.0        :  0.31.2 (OK)
rtree >=0.8.3                :  0.8.3 (OK)
```



Discussion:
@dalthviz, please check if you're able to reproduce this one.
@ccordoba12 I can't reproduce this one (but maybe I'm not understanding how to trigger the error?):

![parenthesis](https://user-images.githubusercontent.com/16781833/71758706-2ed34000-2e71-11ea-8b0a-b48871a974c0.gif)


@abizzle85 could you maybe upload a .gif for a better understanding of the way to get the extra parenthesis? (you can use something like LICECap) Thanks!


@dalthviz  Yes I was able to.  It happens when typing over the closing parenthesis in ".max()".  Usually when I type over auto-generated parenthesis, it understands that parenthesis was already created, and does not duplicate it.  Unless you are using the arrow key to skip over the closing parenthesis?  

![paren_example](https://user-images.githubusercontent.com/59481363/71899053-e776d900-3128-11ea-93be-cdfce5252fd6.gif)

Thanks @abizzle85 for the feedback, now I understand the issue and I can replicate it :+1:

@ccordoba12 should I work on this one?
Yes, please do. 

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
@@ -39,7 +39,8 @@ def _on_key_pressed(self, event):
             self.editor.document_did_change()
             event.accept()
 
-    def unmatched_brackets_in_line(self, text, closing_brackets_type=None):
+    def unmatched_brackets_in_line(self, text, closing_brackets_type=None,
+                                   autoinsert=False):
         """
         Checks if there is an unmatched brackets in the 'text'.
 
@@ -64,7 +65,10 @@ def unmatched_brackets_in_line(self, text, closing_brackets_type=None):
             if char in closing_brackets:
                 match = self.editor.find_brace_match(line_pos+pos, char,
                                                      forward=False)
-                if (match is None) or (match < line_pos):
+                # Only validate match existence for autoinsert.
+                # Having the missing bracket in a previous line is possible.
+                # See spyder-ide/spyder#11217
+                if (match is None) or (match < line_pos and not autoinsert):
                     return True
         return False
 
@@ -101,7 +105,7 @@ def _autoinsert_brackets(self, char):
             if (self.editor.next_char() == char and
                     not self.editor.textCursor().atBlockEnd() and
                     not self.unmatched_brackets_in_line(
-                        cursor.block().text(), char)):
+                        cursor.block().text(), char, autoinsert=True)):
                 # Overwrite an existing brackets if all in line are matched
                 cursor.movePosition(QTextCursor.NextCharacter,
                                     QTextCursor.KeepAnchor, 1)


Test patch:
diff --git a/spyder/plugins/editor/extensions/tests/test_closebrackets.py b/spyder/plugins/editor/extensions/tests/test_closebrackets.py
--- a/spyder/plugins/editor/extensions/tests/test_closebrackets.py
+++ b/spyder/plugins/editor/extensions/tests/test_closebrackets.py
@@ -35,6 +35,26 @@ def editor_close_brackets():
 # =============================================================================
 # ---- Tests
 # =============================================================================
+def test_bracket_closing_new_line(qtbot, editor_close_brackets):
+    """
+    Test bracket completion with existing brackets in a new line.
+
+    For spyder-ide/spyder#11217
+    """
+    editor = editor_close_brackets
+    editor.textCursor().insertText('foo(\nbar)')
+
+    # Move from 'foo(\nbar)|' to 'foo(\nbar|)'
+    editor.move_cursor(-1)
+
+    qtbot.keyClicks(editor, '()')
+    assert editor.toPlainText() == 'foo(\nbar())'
+    assert editor.textCursor().columnNumber() == 5
+    qtbot.keyClicks(editor, ')')
+    assert editor.toPlainText() == 'foo(\nbar())'
+    assert editor.textCursor().columnNumber() == 6
+
+
 @pytest.mark.parametrize(
     'text, expected_text, cursor_column',
     [