-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8637 from bcolsen/closingbrackets
PR: Make a closebrackets extension for smarter brackets
- Loading branch information
Showing
4 changed files
with
318 additions
and
110 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# -*- 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_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: "[]"} | ||
|
||
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_brackets_type=None): | ||
""" | ||
Checks if there is an unmatched brackets in the 'text'. | ||
The brackets type can be general or specified by closing_brackets_type | ||
(')', ']' or '}') | ||
""" | ||
if closing_brackets_type is None: | ||
opening_brackets = self.BRACKETS_LEFT.values() | ||
closing_brackets = self.BRACKETS_RIGHT.values() | ||
else: | ||
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_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_brackets: | ||
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 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 self.BRACKETS_RIGHT.values() or | ||
trailing_text[0] in [',', ':', ';']): | ||
# Automatic insertion of brackets | ||
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) |
194 changes: 194 additions & 0 deletions
194
spyder/plugins/editor/extensions/tests/test_closebrackets.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright © Spyder Project Contributors | ||
# Licensed under the terms of the MIT License | ||
# | ||
"""Tests for close brackets.""" | ||
|
||
# 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 brackets.""" | ||
editor = editor_close_brackets | ||
|
||
qtbot.keyClicks(editor, text) | ||
assert editor.toPlainText() == 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 | ||
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_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 | ||
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, ')') | ||
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() |
Oops, something went wrong.