From 131fbee178c2a29d60596b80c57933ec27981205 Mon Sep 17 00:00:00 2001 From: bcolsen Date: Thu, 24 Jan 2019 00:40:44 -0700 Subject: [PATCH 1/6] Make a closebrackets extension for smarter brackets --- .../editor/extensions/closebrackets.py | 116 ++++++++++++++++++ spyder/plugins/editor/widgets/codeeditor.py | 87 ++----------- 2 files changed, 127 insertions(+), 76 deletions(-) create mode 100644 spyder/plugins/editor/extensions/closebrackets.py diff --git a/spyder/plugins/editor/extensions/closebrackets.py b/spyder/plugins/editor/extensions/closebrackets.py new file mode 100644 index 00000000000..21628c54784 --- /dev/null +++ b/spyder/plugins/editor/extensions/closebrackets.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) +"""This module contains the close quotes editor extension.""" + +from qtpy.QtCore import Qt +from qtpy.QtGui import QTextCursor + +from spyder.api.editorextension import EditorExtension + + +class CloseBracketsExtension(EditorExtension): + """Editor Extension for insert brackets automatically.""" + + BRACKETS_PAIR = {Qt.Key_ParenLeft: "()", Qt.Key_ParenRight: "()", + Qt.Key_BraceLeft: "{}", Qt.Key_BraceRight: "{}", + Qt.Key_BracketLeft: "[]", Qt.Key_BracketRight: "[]", + Qt.Key_Less: "<>", Qt.Key_Greater: "<>"} + BRACKETS_LEFT = {Qt.Key_ParenLeft: "(", + Qt.Key_BraceLeft: "{", + Qt.Key_BracketLeft: "[", + Qt.Key_Less: "<"} + BRACKETS_RIGHT = {Qt.Key_ParenRight: ")", + Qt.Key_BraceRight: "}", + Qt.Key_BracketRight: "]", + Qt.Key_Greater: ">"} + BRACKETS_CHAR = BRACKETS_LEFT.copy() + BRACKETS_CHAR.update(BRACKETS_RIGHT) + + def on_state_changed(self, state): + """Connect/disconnect sig_key_pressed signal.""" + if state: + self.editor.sig_key_pressed.connect(self._on_key_pressed) + else: + self.editor.sig_key_pressed.disconnect(self._on_key_pressed) + + def _on_key_pressed(self, event): + if event.isAccepted(): + return + + key = event.key() + if key in self.BRACKETS_CHAR and self.enabled: + self._autoinsert_brackets(key) + event.accept() + + def unmatched_brackets_in_line(self, text, closing_braces_type=None): + """ + Checks if there is an unmatched brackets in the 'text'. + The brackets type can be general or specified by closing_braces_type + (')', ']', '}' or '>') + """ + if closing_braces_type is None: + opening_braces = self.BRACKETS_LEFT.items() + closing_braces = self.BRACKETS_RIGHT.items() + else: + closing_braces = [closing_braces_type] + opening_braces = [{')': '(', '}': '{', + ']': '[', '>': '<'}[closing_braces_type]] + block = self.editor.textCursor().block() + line_pos = block.position() + for pos, char in enumerate(text): + if char in opening_braces: + match = self.editor.find_brace_match(line_pos+pos, char, forward=True) + if (match is None) or (match > line_pos+len(text)): + return True + if char in closing_braces: + match = self.editor.find_brace_match(line_pos+pos, char, forward=False) + if (match is None) or (match < line_pos): + return True + return False + + def _autoinsert_brackets(self, key): + """Control automatic insertation of brackets in various situations.""" + char = self.BRACKETS_CHAR[key] + pair = self.BRACKETS_PAIR[key] + + line_text = self.editor.get_text('sol', 'eol') + line_to_cursor = self.editor.get_text('sol', 'cursor') + cursor = self.editor.textCursor() + trailing_text = self.editor.get_text('cursor', 'eol').strip() + + if self.editor.has_selected_text(): + text = self.editor.get_selected_text() + self.editor.insert_text("{0}{1}{2}".format(pair[0], text, pair[1])) + # Keep text selected, for inserting multiple quotes + cursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, 1) + cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, + len(text)) + self.editor.setTextCursor(cursor) + elif key in self.BRACKETS_LEFT: + if (not trailing_text or + trailing_text[0] in BRACKETS_RIGHT.items() + [',']): + # Automatic insertion of quotes + self.editor.insert_text(pair) + cursor.movePosition(QTextCursor.PreviousCharacter) + self.editor.setTextCursor(cursor) + else: + self.editor.insert_text(char) + if char in self.editor.signature_completion_characters: + self.editor.request_signature() + elif key in self.BRACKETS_RIGHT: + if (self.editor.next_char() == char and + not self.editor.textCursor().atBlockEnd() and + not self.unmatched_brackets_in_line( + cursor.block().text(), char)): + # Overwrite an existing brackets if all in line are matched + cursor.movePosition(QTextCursor.NextCharacter, + QTextCursor.KeepAnchor, 1) + cursor.clearSelection() + self.editor.setTextCursor(cursor) + else: + self.editor.insert_text(char) + + diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py index c7c609f04a9..44db3318e6d 100644 --- a/spyder/plugins/editor/widgets/codeeditor.py +++ b/spyder/plugins/editor/widgets/codeeditor.py @@ -71,6 +71,8 @@ EditorExtensionsManager) from spyder.plugins.editor.extensions.closequotes import ( CloseQuotesExtension) +from spyder.plugins.editor.extensions.closebrackets import ( + CloseBracketsExtension) from spyder.plugins.editor.api.decoration import TextDecoration from spyder.plugins.editor.utils.lsp import ( request, handles, class_register) @@ -501,6 +503,7 @@ def __init__(self, parent=None): self.editor_extensions = EditorExtensionsManager(self) self.editor_extensions.add(CloseQuotesExtension()) + self.editor_extensions.add(CloseBracketsExtension()) # ---- Keyboard Shortcuts @@ -954,6 +957,9 @@ def set_go_to_definition_enabled(self, enable): def set_close_parentheses_enabled(self, enable): """Enable/disable automatic parentheses insertion feature""" self.close_parentheses_enabled = enable + bracket_extension = self.editor_extensions.get(CloseBracketsExtension) + if bracket_extension is not None: + bracket_extension.enabled = enable def set_close_quotes_enabled(self, enable): """Enable/disable automatic quote insertion feature""" @@ -2633,32 +2639,6 @@ def __forbidden_colon_end_char(self, text): else: return False - def __unmatched_braces_in_line(self, text, closing_braces_type=None): - """ - Checks if there is an unmatched brace in the 'text'. - The brace type can be general or specified by closing_braces_type - (')', ']', or '}') - """ - if closing_braces_type is None: - opening_braces = ['(', '[', '{'] - closing_braces = [')', ']', '}'] - else: - closing_braces = [closing_braces_type] - opening_braces = [{')': '(', '}': '{', - ']': '['}[closing_braces_type]] - block = self.textCursor().block() - line_pos = block.position() - for pos, char in enumerate(text): - if char in opening_braces: - match = self.find_brace_match(line_pos+pos, char, forward=True) - if (match is None) or (match > line_pos+len(text)): - return True - if char in closing_braces: - match = self.find_brace_match(line_pos+pos, char, forward=False) - if (match is None) or (match < line_pos): - return True - return False - def __has_colon_not_in_brackets(self, text): """ Return whether a string has a colon which is not between brackets. @@ -2666,13 +2646,16 @@ def __has_colon_not_in_brackets(self, text): not between a pair of (round, square or curly) brackets. It assumes that the brackets in the string are balanced. """ + bracket_extension = self.editor_extensions.get(CloseBracketsExtension) for pos, char in enumerate(text): - if char == ':' and not self.__unmatched_braces_in_line(text[:pos]): + if (char == ':' and + not bracket_extension.unmatched_brackets_in_line(text[:pos])): return True return False def autoinsert_colons(self): """Decide if we want to autoinsert colons""" + bracket_extension = self.editor_extensions.get(CloseBracketsExtension) line_text = self.get_text('sol', 'cursor') if not self.textCursor().atBlockEnd(): return False @@ -2682,7 +2665,7 @@ def autoinsert_colons(self): return False elif self.__forbidden_colon_end_char(line_text): return False - elif self.__unmatched_braces_in_line(line_text): + elif bracket_extension.unmatched_brackets_in_line(line_text): return False elif self.__has_colon_not_in_brackets(line_text): return False @@ -2925,39 +2908,6 @@ def keyPressEvent(self, event): not self.has_selected_text()): self.insert_text(text) self.request_signature() - elif (text == '(' and - not self.has_selected_text()): - self.handle_parentheses(text) - elif (text in ('[', '{') and not has_selection and - self.close_parentheses_enabled): - s_trailing_text = self.get_text('cursor', 'eol').strip() - if len(s_trailing_text) == 0 or \ - s_trailing_text[0] in (',', ')', ']', '}'): - self.insert_text({'{': '{}', '[': '[]'}[text]) - cursor = self.textCursor() - cursor.movePosition(QTextCursor.PreviousCharacter) - self.setTextCursor(cursor) - else: - TextEditBaseWidget.keyPressEvent(self, event) - elif key in (Qt.Key_ParenRight, Qt.Key_BraceRight, Qt.Key_BracketRight)\ - and not has_selection and self.close_parentheses_enabled \ - and not self.textCursor().atBlockEnd(): - cursor = self.textCursor() - cursor.movePosition(QTextCursor.NextCharacter, - QTextCursor.KeepAnchor) - text = to_text_string(cursor.selectedText()) - key_matches_next_char = ( - text == {Qt.Key_ParenRight: ')', Qt.Key_BraceRight: '}', - Qt.Key_BracketRight: ']'}[key] - ) - if (key_matches_next_char - and not self.__unmatched_braces_in_line( - cursor.block().text(), text)): - # overwrite an existing brace if all braces in line are matched - cursor.clearSelection() - self.setTextCursor(cursor) - else: - TextEditBaseWidget.keyPressEvent(self, event) elif key == Qt.Key_Colon and not has_selection \ and self.auto_unindent_enabled: leading_text = self.get_text('sol', 'cursor') @@ -3013,21 +2963,6 @@ def run_pygments_highlighter(self): if isinstance(self.highlighter, sh.PygmentsSH): self.highlighter.make_charlist() - def handle_parentheses(self, text): - """Handle left and right parenthesis depending on editor config.""" - # position = self.get_position('cursor') - rest = self.get_text('cursor', 'eol').rstrip() - valid = not rest or rest[0] in (',', ')', ']', '}') - if self.close_parentheses_enabled and valid: - self.insert_text('()') - cursor = self.textCursor() - cursor.movePosition(QTextCursor.PreviousCharacter) - self.setTextCursor(cursor) - else: - self.insert_text(text) - if '(' in self.signature_completion_characters: - self.request_signature() - def mouseMoveEvent(self, event): """Underline words when pressing """ self.mouse_point = event.pos() From bc53d9788fa8f8f2fe0f3bd965eef23a9ee57827 Mon Sep 17 00:00:00 2001 From: bcolsen Date: Thu, 24 Jan 2019 22:43:02 -0700 Subject: [PATCH 2/6] pep and test fixes --- .../editor/extensions/closebrackets.py | 37 +++++++++---------- spyder/plugins/editor/widgets/codeeditor.py | 10 ++--- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/spyder/plugins/editor/extensions/closebrackets.py b/spyder/plugins/editor/extensions/closebrackets.py index 21628c54784..9debd215c34 100644 --- a/spyder/plugins/editor/extensions/closebrackets.py +++ b/spyder/plugins/editor/extensions/closebrackets.py @@ -13,22 +13,19 @@ class CloseBracketsExtension(EditorExtension): """Editor Extension for insert brackets automatically.""" - + BRACKETS_PAIR = {Qt.Key_ParenLeft: "()", Qt.Key_ParenRight: "()", Qt.Key_BraceLeft: "{}", Qt.Key_BraceRight: "{}", - Qt.Key_BracketLeft: "[]", Qt.Key_BracketRight: "[]", - Qt.Key_Less: "<>", Qt.Key_Greater: "<>"} + Qt.Key_BracketLeft: "[]", Qt.Key_BracketRight: "[]"} BRACKETS_LEFT = {Qt.Key_ParenLeft: "(", Qt.Key_BraceLeft: "{", - Qt.Key_BracketLeft: "[", - Qt.Key_Less: "<"} + Qt.Key_BracketLeft: "["} BRACKETS_RIGHT = {Qt.Key_ParenRight: ")", Qt.Key_BraceRight: "}", - Qt.Key_BracketRight: "]", - Qt.Key_Greater: ">"} + Qt.Key_BracketRight: "]"} BRACKETS_CHAR = BRACKETS_LEFT.copy() BRACKETS_CHAR.update(BRACKETS_RIGHT) - + def on_state_changed(self, state): """Connect/disconnect sig_key_pressed signal.""" if state: @@ -52,21 +49,23 @@ def unmatched_brackets_in_line(self, text, closing_braces_type=None): (')', ']', '}' or '>') """ if closing_braces_type is None: - opening_braces = self.BRACKETS_LEFT.items() - closing_braces = self.BRACKETS_RIGHT.items() + opening_braces = self.BRACKETS_LEFT.values() + closing_braces = self.BRACKETS_RIGHT.values() else: closing_braces = [closing_braces_type] opening_braces = [{')': '(', '}': '{', - ']': '[', '>': '<'}[closing_braces_type]] + ']': '['}[closing_braces_type]] block = self.editor.textCursor().block() line_pos = block.position() for pos, char in enumerate(text): if char in opening_braces: - match = self.editor.find_brace_match(line_pos+pos, char, forward=True) + match = self.editor.find_brace_match(line_pos+pos, char, + forward=True) if (match is None) or (match > line_pos+len(text)): return True if char in closing_braces: - match = self.editor.find_brace_match(line_pos+pos, char, forward=False) + match = self.editor.find_brace_match(line_pos+pos, char, + forward=False) if (match is None) or (match < line_pos): return True return False @@ -90,8 +89,8 @@ def _autoinsert_brackets(self, key): len(text)) self.editor.setTextCursor(cursor) elif key in self.BRACKETS_LEFT: - if (not trailing_text or - trailing_text[0] in BRACKETS_RIGHT.items() + [',']): + if (not trailing_text or + trailing_text[0] in BRACKETS_RIGHT.items() + [',']): # Automatic insertion of quotes self.editor.insert_text(pair) cursor.movePosition(QTextCursor.PreviousCharacter) @@ -101,16 +100,14 @@ def _autoinsert_brackets(self, key): if char in self.editor.signature_completion_characters: self.editor.request_signature() elif key in self.BRACKETS_RIGHT: - if (self.editor.next_char() == char and - not self.editor.textCursor().atBlockEnd() and + if (self.editor.next_char() == char and + not self.editor.textCursor().atBlockEnd() and not self.unmatched_brackets_in_line( cursor.block().text(), char)): - # Overwrite an existing brackets if all in line are matched + # Overwrite an existing brackets if all in line are matched cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 1) cursor.clearSelection() self.editor.setTextCursor(cursor) else: self.editor.insert_text(char) - - diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py index 44db3318e6d..739208018d8 100644 --- a/spyder/plugins/editor/widgets/codeeditor.py +++ b/spyder/plugins/editor/widgets/codeeditor.py @@ -2646,16 +2646,16 @@ def __has_colon_not_in_brackets(self, text): not between a pair of (round, square or curly) brackets. It assumes that the brackets in the string are balanced. """ - bracket_extension = self.editor_extensions.get(CloseBracketsExtension) + bracket_ext = self.editor_extensions.get(CloseBracketsExtension) for pos, char in enumerate(text): - if (char == ':' and - not bracket_extension.unmatched_brackets_in_line(text[:pos])): + if (char == ':' and + not bracket_ext.unmatched_brackets_in_line(text[:pos])): return True return False def autoinsert_colons(self): """Decide if we want to autoinsert colons""" - bracket_extension = self.editor_extensions.get(CloseBracketsExtension) + bracket_ext = self.editor_extensions.get(CloseBracketsExtension) line_text = self.get_text('sol', 'cursor') if not self.textCursor().atBlockEnd(): return False @@ -2665,7 +2665,7 @@ def autoinsert_colons(self): return False elif self.__forbidden_colon_end_char(line_text): return False - elif bracket_extension.unmatched_brackets_in_line(line_text): + elif bracket_ext.unmatched_brackets_in_line(line_text): return False elif self.__has_colon_not_in_brackets(line_text): return False From e9cc36ef8a70f81cc93906c72ca08475ed9a830f Mon Sep 17 00:00:00 2001 From: bcolsen Date: Sat, 26 Jan 2019 01:24:25 -0700 Subject: [PATCH 3/6] Added tests --- .../editor/extensions/closebrackets.py | 25 ++- .../extensions/tests/test_closebrackets.py | 151 ++++++++++++++++++ 2 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 spyder/plugins/editor/extensions/tests/test_closebrackets.py diff --git a/spyder/plugins/editor/extensions/closebrackets.py b/spyder/plugins/editor/extensions/closebrackets.py index 9debd215c34..d120ef4b9f9 100644 --- a/spyder/plugins/editor/extensions/closebrackets.py +++ b/spyder/plugins/editor/extensions/closebrackets.py @@ -13,18 +13,16 @@ class CloseBracketsExtension(EditorExtension): """Editor Extension for insert brackets automatically.""" - + BRACKETS_LIST = ["(", ")", "{", "}", "[", "]"] + BRACKETS_KEYS = [Qt.Key_ParenLeft, Qt.Key_ParenRight, + Qt.Key_BraceLeft, Qt.Key_BraceRight, + Qt.Key_BracketLeft, Qt.Key_BracketRight] + BRACKETS_CHAR = dict(zip(BRACKETS_KEYS, BRACKETS_LIST)) + BRACKETS_LEFT = dict(zip(BRACKETS_KEYS[::2], BRACKETS_LIST[::2])) + BRACKETS_RIGHT = dict(zip(BRACKETS_KEYS[1::2], BRACKETS_LIST[1::2])) BRACKETS_PAIR = {Qt.Key_ParenLeft: "()", Qt.Key_ParenRight: "()", Qt.Key_BraceLeft: "{}", Qt.Key_BraceRight: "{}", Qt.Key_BracketLeft: "[]", Qt.Key_BracketRight: "[]"} - BRACKETS_LEFT = {Qt.Key_ParenLeft: "(", - Qt.Key_BraceLeft: "{", - Qt.Key_BracketLeft: "["} - BRACKETS_RIGHT = {Qt.Key_ParenRight: ")", - Qt.Key_BraceRight: "}", - Qt.Key_BracketRight: "]"} - BRACKETS_CHAR = BRACKETS_LEFT.copy() - BRACKETS_CHAR.update(BRACKETS_RIGHT) def on_state_changed(self, state): """Connect/disconnect sig_key_pressed signal.""" @@ -46,7 +44,7 @@ def unmatched_brackets_in_line(self, text, closing_braces_type=None): """ Checks if there is an unmatched brackets in the 'text'. The brackets type can be general or specified by closing_braces_type - (')', ']', '}' or '>') + (')', ']' or '}') """ if closing_braces_type is None: opening_braces = self.BRACKETS_LEFT.values() @@ -83,15 +81,16 @@ def _autoinsert_brackets(self, key): if self.editor.has_selected_text(): text = self.editor.get_selected_text() self.editor.insert_text("{0}{1}{2}".format(pair[0], text, pair[1])) - # Keep text selected, for inserting multiple quotes + # Keep text selected, for inserting multiple brackets cursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, 1) cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, len(text)) self.editor.setTextCursor(cursor) elif key in self.BRACKETS_LEFT: if (not trailing_text or - trailing_text[0] in BRACKETS_RIGHT.items() + [',']): - # Automatic insertion of quotes + trailing_text[0] in self.BRACKETS_RIGHT.values() or + trailing_text[0] == ','): + # Automatic insertion of brackets self.editor.insert_text(pair) cursor.movePosition(QTextCursor.PreviousCharacter) self.editor.setTextCursor(cursor) diff --git a/spyder/plugins/editor/extensions/tests/test_closebrackets.py b/spyder/plugins/editor/extensions/tests/test_closebrackets.py new file mode 100644 index 00000000000..c53e18f82ae --- /dev/null +++ b/spyder/plugins/editor/extensions/tests/test_closebrackets.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# +"""Tests for close quotes.""" + +# Third party imports +import pytest +from qtpy.QtGui import QTextCursor + +# Local imports +from spyder.utils.qthelpers import qapplication +from spyder.plugins.editor.widgets.codeeditor import CodeEditor +from spyder.plugins.editor.utils.editor import TextHelper +from spyder.plugins.editor.extensions.closebrackets import ( + CloseBracketsExtension) + + +# --- Fixtures +# ----------------------------------------------------------------------------- +@pytest.fixture +def editor_close_brackets(): + """Set up Editor with close brackets activated.""" + app = qapplication() + editor = CodeEditor(parent=None) + kwargs = {} + kwargs['language'] = 'Python' + kwargs['close_parentheses'] = True + editor.setup_editor(**kwargs) + return editor + +# --- Tests +# ----------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + 'text, expected_text, cursor_column', + [ + ("(", "()", 1), # Close brackets + ("{", "{}", 1), + ("[", "[]", 1), + ]) +def test_close_brackets(qtbot, editor_close_brackets, text, expected_text, + cursor_column): + """Test insertion of extra quotes.""" + editor = editor_close_brackets + + qtbot.keyClicks(editor, text) + assert editor.toPlainText() == expected_text + + assert cursor_column == TextHelper(editor).current_column_nbr() + + +def test_selected_text(qtbot, editor_close_brackets): + """Test insert surronding brackets to selected text.""" + editor = editor_close_brackets + editor.set_text("some text") + + # select some + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 4) + editor.setTextCursor(cursor) + + qtbot.keyClicks(editor, "(") + assert editor.toPlainText() == "(some) text" + + qtbot.keyClicks(editor, "}") + assert editor.toPlainText() == "({some}) text" + + qtbot.keyClicks(editor, "[") + assert editor.toPlainText() == "({[some]}) text" + + +def test_selected_text_multiple_lines(qtbot, editor_close_brackets): + """Test insert surronding brackets to multiple lines selected text.""" + editor = editor_close_brackets + text = ("some text\n" + "\n" + "some text") + editor.set_text(text) + + # select until second some + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 4) + cursor.movePosition(QTextCursor.Down, QTextCursor.KeepAnchor, 2) + editor.setTextCursor(cursor) + + qtbot.keyClicks(editor, ")") + assert editor.toPlainText() == ("(some text\n" + "\n" + "some) text") + + qtbot.keyClicks(editor, "{") + assert editor.toPlainText() == ("({some text\n" + "\n" + "some}) text") + + qtbot.keyClicks(editor, "]") + assert editor.toPlainText() == ("({[some text\n" + "\n" + "some]}) text") + +def test_nested_completion(qtbot, editor_close_brackets): + editor = editor_close_brackets + editor.textCursor().insertText('foo(bar)') + editor.move_cursor(-1) + qtbot.keyClicks(editor, '(') + assert editor.toPlainText() == 'foo(bar())' + assert editor.textCursor().columnNumber() == 8 + +def test_bracket_closing(qtbot, editor_close_brackets): + editor = editor_close_brackets + editor.textCursor().insertText('foo(bar(x') + qtbot.keyClicks(editor, ')') + assert editor.toPlainText() == 'foo(bar(x)' + assert editor.textCursor().columnNumber() == 10 + qtbot.keyClicks(editor, ')') + assert editor.toPlainText() == 'foo(bar(x))' + assert editor.textCursor().columnNumber() == 11 + # same ')' closing with existing brackets starting at 'foo(bar(x|))' + editor.move_cursor(-2) + qtbot.keyClicks(editor, ')') + assert editor.toPlainText() == 'foo(bar(x))' + assert editor.textCursor().columnNumber() == 10 + qtbot.keyClicks(editor, ')') + assert editor.toPlainText() == 'foo(bar(x))' + assert editor.textCursor().columnNumber() == 11 + + +def test_activate_deactivate(qtbot, editor_close_brackets): + """Test activating/desctivating close quotes editor extension.""" + editor = editor_close_brackets + bracket_extension = editor.editor_extensions.get(CloseBracketsExtension) + + qtbot.keyClicks(editor, "(") + assert editor.toPlainText() == "()" + + editor.set_text("") + bracket_extension.enabled = False + qtbot.keyClicks(editor, "(") + assert editor.toPlainText() == "(" + + editor.set_text("") + bracket_extension.enabled = True + qtbot.keyClicks(editor, "(") + assert editor.toPlainText() == "()" + + +if __name__ == '__main__': + pytest.main() From 2a38f24d918b3d5645033765b2994fa185766e7b Mon Sep 17 00:00:00 2001 From: bcolsen Date: Sun, 27 Jan 2019 00:56:59 -0700 Subject: [PATCH 4/6] Removed duplicate tests and general cleanup --- .../editor/extensions/closebrackets.py | 25 +++++++------- .../extensions/tests/test_closebrackets.py | 33 +++++++++++++----- .../editor/widgets/tests/test_codeeditor.py | 34 ------------------- 3 files changed, 38 insertions(+), 54 deletions(-) diff --git a/spyder/plugins/editor/extensions/closebrackets.py b/spyder/plugins/editor/extensions/closebrackets.py index d120ef4b9f9..7c2b857f7ab 100644 --- a/spyder/plugins/editor/extensions/closebrackets.py +++ b/spyder/plugins/editor/extensions/closebrackets.py @@ -40,28 +40,29 @@ def _on_key_pressed(self, event): self._autoinsert_brackets(key) event.accept() - def unmatched_brackets_in_line(self, text, closing_braces_type=None): + def unmatched_brackets_in_line(self, text, closing_brackets_type=None): """ Checks if there is an unmatched brackets in the 'text'. - The brackets type can be general or specified by closing_braces_type + + The brackets type can be general or specified by closing_brackets_type (')', ']' or '}') """ - if closing_braces_type is None: - opening_braces = self.BRACKETS_LEFT.values() - closing_braces = self.BRACKETS_RIGHT.values() + if closing_brackets_type is None: + opening_brackets = self.BRACKETS_LEFT.values() + closing_brackets = self.BRACKETS_RIGHT.values() else: - closing_braces = [closing_braces_type] - opening_braces = [{')': '(', '}': '{', - ']': '['}[closing_braces_type]] + closing_brackets = [closing_brackets_type] + opening_brackets = [{')': '(', '}': '{', + ']': '['}[closing_brackets_type]] block = self.editor.textCursor().block() line_pos = block.position() for pos, char in enumerate(text): - if char in opening_braces: + if char in opening_brackets: match = self.editor.find_brace_match(line_pos+pos, char, forward=True) if (match is None) or (match > line_pos+len(text)): return True - if char in closing_braces: + 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): @@ -100,8 +101,8 @@ def _autoinsert_brackets(self, key): self.editor.request_signature() elif key in self.BRACKETS_RIGHT: if (self.editor.next_char() == char and - not self.editor.textCursor().atBlockEnd() and - not self.unmatched_brackets_in_line( + not self.editor.textCursor().atBlockEnd() and + not self.unmatched_brackets_in_line( cursor.block().text(), char)): # Overwrite an existing brackets if all in line are matched cursor.movePosition(QTextCursor.NextCharacter, diff --git a/spyder/plugins/editor/extensions/tests/test_closebrackets.py b/spyder/plugins/editor/extensions/tests/test_closebrackets.py index c53e18f82ae..c0d672e9512 100644 --- a/spyder/plugins/editor/extensions/tests/test_closebrackets.py +++ b/spyder/plugins/editor/extensions/tests/test_closebrackets.py @@ -3,7 +3,7 @@ # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # -"""Tests for close quotes.""" +"""Tests for close brackets.""" # Third party imports import pytest @@ -17,8 +17,9 @@ CloseBracketsExtension) -# --- Fixtures -# ----------------------------------------------------------------------------- +# ============================================================================= +# ---- Fixtures +# ============================================================================= @pytest.fixture def editor_close_brackets(): """Set up Editor with close brackets activated.""" @@ -30,10 +31,10 @@ def editor_close_brackets(): editor.setup_editor(**kwargs) return editor -# --- Tests -# ----------------------------------------------------------------------------- - +# ============================================================================= +# ---- Tests +# ============================================================================= @pytest.mark.parametrize( 'text, expected_text, cursor_column', [ @@ -42,8 +43,8 @@ def editor_close_brackets(): ("[", "[]", 1), ]) def test_close_brackets(qtbot, editor_close_brackets, text, expected_text, - cursor_column): - """Test insertion of extra quotes.""" + cursor_column): + """Test insertion of brackets.""" editor = editor_close_brackets qtbot.keyClicks(editor, text) @@ -101,15 +102,31 @@ def test_selected_text_multiple_lines(qtbot, editor_close_brackets): "\n" "some]}) text") + def test_nested_completion(qtbot, editor_close_brackets): + """Test bracket completion in nested brackets.""" editor = editor_close_brackets + # Test completion when following character is a right bracket editor.textCursor().insertText('foo(bar)') editor.move_cursor(-1) qtbot.keyClicks(editor, '(') assert editor.toPlainText() == 'foo(bar())' assert editor.textCursor().columnNumber() == 8 + # Test normal insertion when next character is not a right bracket + editor.move_cursor(-1) + qtbot.keyClicks(editor, '[') + assert editor.toPlainText() == 'foo(bar[())' + assert editor.textCursor().columnNumber() == 8 + # Test completion when following character is a comma + qtbot.keyClicks(editor, ',') + editor.move_cursor(-1) + qtbot.keyClicks(editor, '{') + assert editor.toPlainText() == 'foo(bar[{},())' + assert editor.textCursor().columnNumber() == 9 + def test_bracket_closing(qtbot, editor_close_brackets): + """Test bracket completion with existing brackets.""" editor = editor_close_brackets editor.textCursor().insertText('foo(bar(x') qtbot.keyClicks(editor, ')') diff --git a/spyder/plugins/editor/widgets/tests/test_codeeditor.py b/spyder/plugins/editor/widgets/tests/test_codeeditor.py index ba5343c3304..0a84ad36bcf 100644 --- a/spyder/plugins/editor/widgets/tests/test_codeeditor.py +++ b/spyder/plugins/editor/widgets/tests/test_codeeditor.py @@ -56,37 +56,3 @@ def test_editor_lower_to_upper(editorbot): widget.transform_to_uppercase() new_text = widget.get_text('sof', 'eof') assert text != new_text - -def test_editor_complete_backet(editorbot): - qtbot, editor = editorbot - editor.textCursor().insertText('foo') - qtbot.keyClicks(editor, '(') - assert editor.toPlainText() == 'foo()' - assert editor.textCursor().columnNumber() == 4 - -def test_editor_complete_bracket_nested(editorbot): - qtbot, editor = editorbot - editor.textCursor().insertText('foo(bar)') - editor.move_cursor(-1) - qtbot.keyClicks(editor, '(') - assert editor.toPlainText() == 'foo(bar())' - assert editor.textCursor().columnNumber() == 8 - -def test_editor_bracket_closing(editorbot): - qtbot, editor = editorbot - editor.textCursor().insertText('foo(bar(x') - qtbot.keyClicks(editor, ')') - assert editor.toPlainText() == 'foo(bar(x)' - assert editor.textCursor().columnNumber() == 10 - qtbot.keyClicks(editor, ')') - assert editor.toPlainText() == 'foo(bar(x))' - assert editor.textCursor().columnNumber() == 11 - # same ')' closing with existing brackets starting at 'foo(bar(x|))' - editor.move_cursor(-2) - qtbot.keyClicks(editor, ')') - assert editor.toPlainText() == 'foo(bar(x))' - assert editor.textCursor().columnNumber() == 10 - qtbot.keyClicks(editor, ')') - assert editor.toPlainText() == 'foo(bar(x))' - assert editor.textCursor().columnNumber() == 11 - From 7135ac0d7ba043d04623c21dfc155bf27e3ac87c Mon Sep 17 00:00:00 2001 From: bcolsen Date: Sun, 27 Jan 2019 01:01:42 -0700 Subject: [PATCH 5/6] pep fix --- spyder/plugins/editor/extensions/closebrackets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/plugins/editor/extensions/closebrackets.py b/spyder/plugins/editor/extensions/closebrackets.py index 7c2b857f7ab..5ca6adeda61 100644 --- a/spyder/plugins/editor/extensions/closebrackets.py +++ b/spyder/plugins/editor/extensions/closebrackets.py @@ -53,7 +53,7 @@ def unmatched_brackets_in_line(self, text, closing_brackets_type=None): else: closing_brackets = [closing_brackets_type] opening_brackets = [{')': '(', '}': '{', - ']': '['}[closing_brackets_type]] + ']': '['}[closing_brackets_type]] block = self.editor.textCursor().block() line_pos = block.position() for pos, char in enumerate(text): From fe8a45e6e20728e4fe57faaef79d618afe4d2a1a Mon Sep 17 00:00:00 2001 From: bcolsen Date: Mon, 28 Jan 2019 23:52:00 -0700 Subject: [PATCH 6/6] added completions for brackets before colons and semi-colons --- .../editor/extensions/closebrackets.py | 2 +- .../extensions/tests/test_closebrackets.py | 28 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/editor/extensions/closebrackets.py b/spyder/plugins/editor/extensions/closebrackets.py index 5ca6adeda61..b4eb91653a3 100644 --- a/spyder/plugins/editor/extensions/closebrackets.py +++ b/spyder/plugins/editor/extensions/closebrackets.py @@ -90,7 +90,7 @@ def _autoinsert_brackets(self, key): elif key in self.BRACKETS_LEFT: if (not trailing_text or trailing_text[0] in self.BRACKETS_RIGHT.values() or - trailing_text[0] == ','): + trailing_text[0] in [',', ':', ';']): # Automatic insertion of brackets self.editor.insert_text(pair) cursor.movePosition(QTextCursor.PreviousCharacter) diff --git a/spyder/plugins/editor/extensions/tests/test_closebrackets.py b/spyder/plugins/editor/extensions/tests/test_closebrackets.py index c0d672e9512..2a998763e1d 100644 --- a/spyder/plugins/editor/extensions/tests/test_closebrackets.py +++ b/spyder/plugins/editor/extensions/tests/test_closebrackets.py @@ -53,6 +53,32 @@ def test_close_brackets(qtbot, editor_close_brackets, text, expected_text, assert cursor_column == TextHelper(editor).current_column_nbr() +@pytest.mark.parametrize( + 'text, expected_text, cursor_column', + [ + ('()', '(())', 2), # Complete in brackets + ('{}', '{()}', 2), + ('[]', '[()]', 2), + (',', '(),', 1), # Complete before commas, colons and semi-colons + (':', '():', 1), + (';', '();', 1), + ]) +def test_nested_brackets(qtbot, editor_close_brackets, text, expected_text, + cursor_column): + """ + Test completion of brackets inside brackets and before commas, + colons and semi-colons. + """ + editor = editor_close_brackets + + qtbot.keyClicks(editor, text) + editor.move_cursor(-1) + qtbot.keyClicks(editor, '(') + assert editor.toPlainText() == expected_text + + assert cursor_column == TextHelper(editor).current_column_nbr() + + def test_selected_text(qtbot, editor_close_brackets): """Test insert surronding brackets to selected text.""" editor = editor_close_brackets @@ -103,7 +129,7 @@ def test_selected_text_multiple_lines(qtbot, editor_close_brackets): "some]}) text") -def test_nested_completion(qtbot, editor_close_brackets): +def test_complex_completion(qtbot, editor_close_brackets): """Test bracket completion in nested brackets.""" editor = editor_close_brackets # Test completion when following character is a right bracket