Skip to content

Commit

Permalink
Merge pull request #8637 from bcolsen/closingbrackets
Browse files Browse the repository at this point in the history
PR: Make a closebrackets extension for smarter brackets
  • Loading branch information
ccordoba12 authored Jan 29, 2019
2 parents 9d7442c + fe8a45e commit 4d7e092
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 110 deletions.
113 changes: 113 additions & 0 deletions spyder/plugins/editor/extensions/closebrackets.py
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 spyder/plugins/editor/extensions/tests/test_closebrackets.py
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()
Loading

0 comments on commit 4d7e092

Please sign in to comment.