Ejemplo n.º 1
0
def test_selected_text(editor):
    helper = TextHelper(editor)
    helper.goto_line(2, 1, move=True)
    QTest.qWait(100)
    assert helper.word_under_cursor().selectedText() == 'T'
    assert helper.word_under_cursor(
        select_whole_word=True).selectedText() == 'This'
Ejemplo n.º 2
0
 def _on_key_pressed(self, event):
     """
     Override key press to select the current scope if the user wants
     to deleted a folded scope (without selecting it).
     """
     delete_request = event.key() in [QtCore.Qt.Key_Backspace,
                                      QtCore.Qt.Key_Delete]
     if event.text() or delete_request:
         cursor = self.editor.textCursor()
         if cursor.hasSelection():
             # change selection to encompass the whole scope.
             positions_to_check = cursor.selectionStart(), cursor.selectionEnd()
         else:
             positions_to_check = (cursor.position(), )
         for pos in positions_to_check:
             block = self.editor.document().findBlock(pos)
             th = TextBlockHelper()
             if th.is_fold_trigger(block) and th.is_collapsed(block):
                 self.toggle_fold_trigger(block)
                 if delete_request and cursor.hasSelection():
                     scope = FoldScope(self.find_parent_scope(block))
                     tc = TextHelper(self.editor).select_lines(*scope.get_range())
                     if tc.selectionStart() > cursor.selectionStart():
                         start = cursor.selectionStart()
                     else:
                         start = tc.selectionStart()
                     if tc.selectionEnd() < cursor.selectionEnd():
                         end = cursor.selectionEnd()
                     else:
                         end = tc.selectionEnd()
                     tc.setPosition(start)
                     tc.setPosition(end, tc.KeepAnchor)
                     self.editor.setTextCursor(tc)
 def request_completion(self):
     """
     Requests a code completion at the current cursor position.
     """
     _logger().debug('request code completion')
     self._col = self.editor.textCursor().positionInBlock() - len(
         self.completion_prefix)
     helper = TextHelper(self.editor)
     if not self._request_cnt:
         # only check first byte
         tc = self.editor.textCursor()
         while tc.atBlockEnd() and not tc.atBlockStart() and \
                 tc.position():
             tc.movePosition(tc.Left)
         disabled_zone = TextHelper(self.editor).is_comment_or_string(
             tc)
         if disabled_zone:
             _logger().debug(
                 "cc: cancel request, cursor is in a disabled zone")
             return False
         self._request_cnt += 1
         self._collect_completions(self.editor.toPlainText(),
                                   helper.current_line_nbr(),
                                   helper.current_column_nbr() -
                                   len(self.completion_prefix),
                                   self.editor.file.path,
                                   self.editor.file.encoding,
                                   self.completion_prefix)
         return True
     return False
Ejemplo n.º 4
0
 def request_completion(self):
     """
     Requests a code completion at the current cursor position.
     """
     _logger().debug('request code completion')
     self._col = self.editor.textCursor().positionInBlock() - len(
         self.completion_prefix)
     helper = TextHelper(self.editor)
     if not self._request_cnt:
         # only check first byte
         tc = self.editor.textCursor()
         while tc.atBlockEnd() and not tc.atBlockStart() and \
                 tc.position():
             tc.movePosition(tc.Left)
         disabled_zone = TextHelper(self.editor).is_comment_or_string(tc)
         if disabled_zone:
             _logger().debug(
                 "cc: cancel request, cursor is in a disabled zone")
             return False
         self._request_cnt += 1
         self._collect_completions(
             self.editor.toPlainText(), helper.current_line_nbr(),
             helper.current_column_nbr() - len(self.completion_prefix),
             self.editor.file.path, self.editor.file.encoding,
             self.completion_prefix)
         return True
     return False
Ejemplo n.º 5
0
def test_clear_selection(editor):
    editor.file.open(__file__)
    helper = TextHelper(editor)
    TextHelper(editor).select_lines(0, 2)
    assert helper.selected_text() != ''
    TextHelper(editor).clear_selection()
    assert helper.selected_text() == ''
Ejemplo n.º 6
0
def test_line_indent(editor):
    editor.setPlainText(src, 'text/x-python', 'utf-8')
    assert TextHelper(editor).line_indent(0) == 0
    assert TextHelper(editor).line_indent(1) == 4
    editor.file.open(__file__)
    assert TextHelper(editor).line_indent(TextHelper(editor).line_count() -
                                          2) == 4
Ejemplo n.º 7
0
def test_line_nbr_from_position(editor):
    editor.repaint()
    sys.stderr.write(str(editor.visible_blocks))
    assert TextHelper(editor).line_nbr_from_position(
        TextHelper(editor).line_pos_from_number(0)) is not None
    assert TextHelper(editor).line_nbr_from_position(-1) == -1
    QTest.qWait(100)
Ejemplo n.º 8
0
 def paintEvent(self, event):
     # Paints the line numbers
     self._line_color_u = drift_color(self._background_brush.color(), 250)
     self._line_color_s = drift_color(self._background_brush.color(), 280)
     Panel.paintEvent(self, event)
     if self.isVisible():
         painter = QtGui.QPainter(self)
         # get style options (font, size)
         width = self.width()
         height = self.editor.fontMetrics().height()
         font = self.editor.font()
         bold_font = self.editor.font()
         bold_font.setBold(True)
         pen = QtGui.QPen(self._line_color_u)
         pen_selected = QtGui.QPen(self._line_color_s)
         painter.setFont(font)
         # get selection range
         sel_start, sel_end = TextHelper(self.editor).selection_range()
         has_sel = sel_start != sel_end
         cl = TextHelper(self.editor).current_line_nbr()
         # draw every visible blocks
         for top, line, block in self.editor.visible_blocks:
             if ((has_sel and sel_start <= line <= sel_end)
                     or (not has_sel and cl == line)):
                 painter.setPen(pen_selected)
                 painter.setFont(bold_font)
             else:
                 painter.setPen(pen)
                 painter.setFont(font)
             painter.drawText(-3, top, width, height, QtCore.Qt.AlignRight,
                              str(line + 1))
Ejemplo n.º 9
0
 def mouseMoveEvent(self, e):
     # Updates end of selection if we are currently selecting
     if self._selecting:
         end_pos = e.pos().y()
         start_line = TextHelper(self.editor).line_nbr_from_position(
             self._sel_start)
         end_line = TextHelper(self.editor).line_nbr_from_position(end_pos)
         TextHelper(self.editor).select_lines(start_line, end_line)
Ejemplo n.º 10
0
def test_line_pos_from_number(editor):
    assert TextHelper(editor).line_pos_from_number(0) is not None
    # out of range line will return the bottom of the document or the top
    assert TextHelper(editor).line_pos_from_number(-1) == 0
    pos = TextHelper(editor).line_pos_from_number(
        TextHelper(editor).line_count() + 10)
    assert pos is not None
    assert pos > 0
Ejemplo n.º 11
0
def test_select_lines(editor):
    TextHelper(editor).select_lines(0, 4)
    QTest.qWait(100)
    QTest.qWait(1000)
    assert TextHelper(editor).selection_range() == (0, 4)
    TextHelper(editor).select_lines(-1, 10)
    assert TextHelper(editor).selection_range() == (0, 10)
    editor.decorations.clear()
Ejemplo n.º 12
0
 def mousePressEvent(self, e):
     """
     Starts selecting
     """
     self._selecting = True
     self._sel_start = e.pos().y()
     start = end = TextHelper(self.editor).line_nbr_from_position(
         self._sel_start)
     TextHelper(self.editor).select_lines(start, end)
Ejemplo n.º 13
0
 def goto_line(self):
     """
     Shows the *go to line dialog* and go to the selected line.
     """
     helper = TextHelper(self)
     line, result = DlgGotoLine.get_line(self, helper.current_line_nbr(), helper.line_count())
     if not result:
         return
     return helper.goto_line(line, move=True)
Ejemplo n.º 14
0
 def on_install(self, editor):
     self._completer = QtWidgets.QCompleter([""], editor)
     self._completer.setCompletionMode(self._completer.PopupCompletion)
     self._completer.activated.connect(self._insert_completion)
     self._completer.highlighted.connect(
         self._on_selected_completion_changed)
     self._completer.setModel(QtGui.QStandardItemModel())
     self._helper = TextHelper(editor)
     Mode.on_install(self, editor)
Ejemplo n.º 15
0
 def goto_line(self):
     """
     Shows the *go to line dialog* and go to the selected line.
     """
     helper = TextHelper(self)
     line, result = DlgGotoLine.get_line(
         self, helper.current_line_nbr(), helper.line_count())
     if not result:
         return
     return helper.goto_line(line, move=True)
Ejemplo n.º 16
0
def test_keep_tc(editor):
    @keep_tc_pos
    def move_cursor(editor, arg):
        assert arg == 'arg'
        TextHelper(editor).goto_line(4)

    l, c = TextHelper(editor).cursor_position()
    move_cursor(editor, 'arg')
    nl, nc = TextHelper(editor).cursor_position()
    assert l == nl and c == nc
Ejemplo n.º 17
0
def test_do_home_key(editor):
    QTest.qWait(2000)
    helper = TextHelper(editor)
    helper.goto_line(336, 29)
    assert editor.textCursor().positionInBlock() == 29
    assert TextHelper(editor).line_indent() == 4
    editor._do_home_key()
    assert editor.textCursor().positionInBlock() == 4
    editor._do_home_key()
    assert editor.textCursor().positionInBlock() == 0
Ejemplo n.º 18
0
 def _on_key_pressed(self, event):
     """
     Override key press to select the current scope if the user wants
     to deleted a folded scope (without selecting it).
     """
     delete_request = event.key() in [QtCore.Qt.Key_Backspace,
                                      QtCore.Qt.Key_Delete]
     if event.text() or delete_request:
         cursor = self.editor.textCursor()
         if cursor.hasSelection():
             # change selection to encompass the whole scope.
             positions_to_check = cursor.selectionStart(), cursor.selectionEnd()
         else:
             positions_to_check = (cursor.position(), )
         for pos in positions_to_check:
             block = self.editor.document().findBlock(pos)
             th = TextBlockHelper()
             if th.is_fold_trigger(block) and th.is_collapsed(block):
                 self.toggle_fold_trigger(block)
                 if delete_request and cursor.hasSelection():
                     scope = FoldScope(self.find_parent_scope(block))
                     tc = TextHelper(self.editor).select_lines(*scope.get_range())
                     if tc.selectionStart() > cursor.selectionStart():
                         start = cursor.selectionStart()
                     else:
                         start = tc.selectionStart()
                     if tc.selectionEnd() < cursor.selectionEnd():
                         end = cursor.selectionEnd()
                     else:
                         end = tc.selectionEnd()
                     tc.setPosition(start)
                     tc.setPosition(end, tc.KeepAnchor)
                     self.editor.setTextCursor(tc)
Ejemplo n.º 19
0
def test_cut_no_selection(editor):
    assert isinstance(editor, CodeEdit)
    editor.setPlainText('''Line 1
Line 2
Line 3''', '', '')
    helper = TextHelper(editor)

    # eat empty line
    helper.goto_line(0)
    assert helper.line_count() == 3
    editor.cut()
    assert helper.line_count() == 2
Ejemplo n.º 20
0
def test_goto_line(editor):
    assert editor.textCursor().blockNumber() == 0
    assert editor.textCursor().columnNumber() == 0
    cursor = TextHelper(editor).goto_line(2, 0, move=False)
    QTest.qWait(100)
    assert editor.textCursor().blockNumber() != cursor.blockNumber()
    assert editor.textCursor().columnNumber() == cursor.columnNumber()
    cursor = TextHelper(editor).goto_line(9, move=True)
    QTest.qWait(100)
    assert editor.textCursor().blockNumber() == cursor.blockNumber() == 9
    assert editor.textCursor().columnNumber() == cursor.columnNumber() == 0
    assert TextHelper(editor).current_line_nbr() == 9
    assert TextHelper(editor).current_column_nbr() == 0
Ejemplo n.º 21
0
 def mouseMoveEvent(self, e):
     # Updates end of selection if we are currently selecting
     if self._selecting:
         end_pos = e.pos().y()
         end_line = TextHelper(self.editor).line_nbr_from_position(end_pos)
         if end_line == -1 and self.editor.visible_blocks:
             # take last visible block
             if end_pos < 50:
                 _, end_line, _ = self.editor.visible_blocks[0]
                 end_line -= 1
             else:
                 _, end_line, _ = self.editor.visible_blocks[-1]
                 end_line += 1
         TextHelper(self.editor).select_lines(self._start_line, end_line)
Ejemplo n.º 22
0
    def provide_ide_current_word(self):

        editor = self._current_editor()
        if editor is None:
            return u''
        return TextHelper(editor).word_under_cursor(
            select_whole_word=True).selectedText()
Ejemplo n.º 23
0
 def _do_home_key(self, event=None, select=False):
     """ Performs home key action """
     cursor = self.textCursor()
     move = QtGui.QTextCursor.MoveAnchor
     if select:
         move = QtGui.QTextCursor.KeepAnchor
     indent = TextHelper(self).line_indent()
     # Scenario 1: We're on an unindented block. In that case, we jump back
     # to the start of the visible line, but not all the way to the back of
     # the block. This is what you would expect when working with text and
     # line wrapping is enabled.
     if not indent:
         cursor.movePosition(QtGui.QTextCursor.StartOfLine, move)
     else:
         delta = self.textCursor().positionInBlock() - indent
         # Scenario 2: We're on an indented block. In that case, we move
         # back to the indented position. This is what you would expect when
         # working with code.
         if delta > 0:
             cursor.movePosition(QtGui.QTextCursor.Left, move, delta)
         # Scenario 3: We're on an indented block, but we're already at the
         # start of the indentation. In that case, we jump back to the
         # beginning of the block.
         else:
             cursor.movePosition(QtGui.QTextCursor.StartOfBlock, move)
     self.setTextCursor(cursor)
     if event:
         event.accept()
Ejemplo n.º 24
0
 def mouseMoveEvent(self, event):
     # Requests a tooltip if the cursor is currently over a marker.
     line = TextHelper(self.editor).line_nbr_from_position(event.pos().y())
     markers = self.marker_for_line(line)
     text = '\n'.join([marker.description for marker in markers if
                       marker.description])
     if len(markers):
         if self._previous_line != line:
             top = TextHelper(self.editor).line_pos_from_number(
                 markers[0].position)
             if top:
                 self._job_runner.request_job(self._display_tooltip,
                                              text, top)
     else:
         self._job_runner.cancel_requests()
     self._previous_line = line
Ejemplo n.º 25
0
 def cut(self):
     tc = self.textCursor()
     helper = TextHelper(self)
     tc.beginEditBlock()
     no_selection = False
     if not helper.current_line_text().strip():
         tc.deleteChar()
     else:
         if not self.textCursor().hasSelection():
             no_selection = True
             TextHelper(self).select_whole_line()
         super(CodeEdit, self).cut()
         if no_selection:
             tc.deleteChar()
     tc.endEditBlock()
     self.setTextCursor(tc)
Ejemplo n.º 26
0
def test_indent(editor):
    # disable indenter mode -> indent should not do anything
    editor.modes.get(modes.IndenterMode).enabled = False
    TextHelper(editor).goto_line(0, move=True)
    first_line = get_first_line(editor)
    editor.indent()
    assert get_first_line(editor) == first_line
    editor.un_indent()
    assert get_first_line(editor) == first_line
    # enable indenter mode, call to indent/un_indent should now work
    editor.modes.get(modes.IndenterMode).enabled = True
    TextHelper(editor).goto_line(0)
    editor.indent()
    assert get_first_line(editor) == editor.tab_length * ' ' + first_line
    editor.un_indent()
    assert get_first_line(editor) == first_line
Ejemplo n.º 27
0
    def __swapLine(self, up: bool):
        helper = TextHelper(self)
        text = helper.current_line_text()
        line_nbr = helper.current_line_nbr()
        if up:
            swap_line_nbr = line_nbr - 1
        else:
            swap_line_nbr = line_nbr + 1
        swap_text = helper.line_text(swap_line_nbr)

        if (swap_line_nbr < helper.line_count()
                and line_nbr < helper.line_count()):
            helper.set_line_text(line_nbr, swap_text)
            helper.set_line_text(swap_line_nbr, text)
Ejemplo n.º 28
0
 def copy(self):
     """
     Copy the selected text to the clipboard. If no text was selected, the
     entire line is copied (this feature can be turned off by
     setting :attr:`select_line_on_copy_empty` to False.
     """
     if self.select_line_on_copy_empty and not self.textCursor().hasSelection():
         TextHelper(self).select_whole_line()
     super(CodeEdit, self).copy()
Ejemplo n.º 29
0
    def zoom_in(self, increment=1):
        """
        Zooms in the editor (makes the font bigger).

        :param increment: zoom level increment. Default is 1.
        """
        self.zoom_level += increment
        TextHelper(self).mark_whole_doc_dirty()
        self._reset_stylesheet()
 def on_install(self, editor):
     self._completer = QtWidgets.QCompleter([""], editor)
     self._completer.setCompletionMode(self._completer.PopupCompletion)
     self._completer.activated.connect(self._insert_completion)
     self._completer.highlighted.connect(
         self._on_selected_completion_changed)
     self._completer.setModel(QtGui.QStandardItemModel())
     self._helper = TextHelper(editor)
     Mode.on_install(self, editor)
Ejemplo n.º 31
0
def test_duplicate_line(editor):
    QTest.qWait(1000)
    TextHelper(editor).goto_line(0)
    editor.duplicate_line()
    assert editor.toPlainText().startswith(get_first_line(editor) + '\n' +
                                           get_first_line(editor))
    editor.setPlainText('foo', '', 'utf-8')
    editor.duplicate_line()
    assert editor.toPlainText() == 'foo\nfoo'
    assert editor.textCursor().position() == 7
Ejemplo n.º 32
0
def test_clean_document(editor):
    TextHelper(editor).clean_document()
    count = TextHelper(editor).line_count()
    TextHelper(editor).set_line_text(0, '"""   ')
    editor.appendPlainText("")
    editor.appendPlainText("")
    editor.appendPlainText("")
    assert TextHelper(editor).line_count() == count + 3
    TextHelper(editor).select_lines(0, TextHelper(editor).line_count())
    TextHelper(editor).clean_document()
    QTest.qWait(100)
    assert TextHelper(editor).line_count() == count
Ejemplo n.º 33
0
 def cut(self):
     """
     Cuts the selected text or the whole line if no text was selected.
     """
     tc = self.textCursor()
     helper = TextHelper(self)
     tc.beginEditBlock()
     no_selection = False
     sText = tc.selection().toPlainText()
     if not helper.current_line_text() and sText.count("\n") > 1:
         tc.deleteChar()
     else:
         if not self.textCursor().hasSelection():
             no_selection = True
             TextHelper(self).select_whole_line()
         super(CodeEdit, self).cut()
         if no_selection:
             tc.deleteChar()
     tc.endEditBlock()
     self.setTextCursor(tc)
Ejemplo n.º 34
0
    def split(self):
        """
        Split the code editor widget, return a clone of the widget ready to
        be used (and synchronised with its original).

        Splitting the widget is done in 2 steps:
            - first we clone the widget, you can override ``clone`` if your
              widget needs additional arguments.

            - then we link the two text document and disable some modes on the
                cloned instance (such as the watcher mode).
        """
        # cache cursor position so that the clone open at the current cursor
        # pos
        l, c = TextHelper(self).cursor_position()
        clone = self.clone()
        self.link(clone)
        TextHelper(clone).goto_line(l, c)
        self.clones.append(clone)
        return clone
Ejemplo n.º 35
0
 def cut(self):
     """
     Cuts the selected text or the whole line if no text was selected.
     """
     tc = self.textCursor()
     helper = TextHelper(self)
     tc.beginEditBlock()
     no_selection = False
     sText = tc.selection().toPlainText()
     if not helper.current_line_text() and sText.count("\n") > 1:
         tc.deleteChar()
     else:
         if not self.textCursor().hasSelection():
             no_selection = True
             TextHelper(self).select_whole_line()
         super(CodeEdit, self).cut()
         if no_selection:
             tc.deleteChar()
     tc.endEditBlock()
     self.setTextCursor(tc)
Ejemplo n.º 36
0
 def _update(self, rect, delta_y, force_update_margins=False):
     """ Updates panels """
     helper = TextHelper(self.editor)
     if not self:
         return
     for zones_id, zone in self._panels.items():
         if zones_id == Panel.Position.TOP or \
            zones_id == Panel.Position.BOTTOM:
             continue
         panels = list(zone.values())
         for panel in panels:
             if panel.scrollable and delta_y:
                 panel.scroll(0, delta_y)
             line, col = helper.cursor_position()
             oline, ocol = self._cached_cursor_pos
             if line != oline or col != ocol or panel.scrollable:
                 panel.update(0, rect.y(), panel.width(), rect.height())
             self._cached_cursor_pos = helper.cursor_position()
     if (rect.contains(self.editor.viewport().rect())
             or force_update_margins):
         self._update_viewport_margins()
Ejemplo n.º 37
0
def test_copy_no_selection(editor):
    """
    Tests the select_line_on_copy_empty option that toggles the
    "whole line selection on copy with empty selection"-feature 
    """
    assert isinstance(editor, CodeEdit)
    editor.setPlainText('''Line 1
Line 2
Line 3''', '', '')
    helper = TextHelper(editor)
    helper.goto_line(0)
    editor.textCursor().clearSelection()

    editor.select_line_on_copy_empty = False
    editor.copy()
    assert editor.textCursor().hasSelection() is False

    editor.textCursor().clearSelection()
    editor.select_line_on_copy_empty = True
    editor.copy()
    assert editor.textCursor().hasSelection()
Ejemplo n.º 38
0
    def zoom_in(self, increment=1):
        """
        Zooms in the editor (makes the font bigger).

        :param increment: zoom level increment. Default is 1.
        """
        # When called through an action, the first argument is a bool
        if isinstance(increment, bool):
            increment = 1
        self.zoom_level += increment
        TextHelper(self).mark_whole_doc_dirty()
        self._reset_stylesheet()
Ejemplo n.º 39
0
 def _update(self, rect, delta_y, force_update_margins=False):
     """ Updates panels """
     helper = TextHelper(self.editor)
     if not self:
         return
     for zones_id, zone in self._panels.items():
         if zones_id == Panel.Position.TOP or \
            zones_id == Panel.Position.BOTTOM:
             continue
         panels = list(zone.values())
         for panel in panels:
             if panel.scrollable and delta_y:
                 panel.scroll(0, delta_y)
             line, col = helper.cursor_position()
             oline, ocol = self._cached_cursor_pos
             if line != oline or col != ocol or panel.scrollable:
                 panel.update(0, rect.y(), panel.width(), rect.height())
             self._cached_cursor_pos = helper.cursor_position()
     if (rect.contains(self.editor.viewport().rect()) or
             force_update_margins):
         self._update_viewport_margins()
Ejemplo n.º 40
0
    def __swapLine(self, up: bool):
        helper = TextHelper(self)
        text = helper.current_line_text()
        line_nbr = helper.current_line_nbr()
        if up:
            swap_line_nbr = line_nbr - 1
        else:
            swap_line_nbr = line_nbr + 1
        swap_text = helper.line_text(swap_line_nbr)

        if (swap_line_nbr < helper.line_count()
                and line_nbr < helper.line_count()):
            helper.set_line_text(line_nbr, swap_text)
            helper.set_line_text(swap_line_nbr, text)
Ejemplo n.º 41
0
    def _draw_fold_region_background(self, block, painter):
        """
        Draw the fold region when the mouse is over and non collapsed
        indicator.

        :param top: Top position
        :param block: Current block.
        :param painter: QPainter
        """
        r = folding.FoldScope(block)
        th = TextHelper(self.editor)
        start, end = r.get_range(ignore_blank_lines=True)
        if start > 0:
            top = th.line_pos_from_number(start)
        else:
            top = 0
        bottom = th.line_pos_from_number(end + 1)
        h = bottom - top
        if h == 0:
            h = self.sizeHint().height()
        w = self.sizeHint().width()
        self._draw_rect(QtCore.QRectF(0, top, w, h), painter)
Ejemplo n.º 42
0
    def mouseMoveEvent(self, event):
        """
        Detect mouser over indicator and highlight the current scope in the
        editor (up and down decoration arround the foldable text when the mouse
        is over an indicator).

        :param event: event
        """
        super(FoldingPanel, self).mouseMoveEvent(event)
        th = TextHelper(self.editor)
        line = th.line_nbr_from_position(event.pos().y())
        if line >= 0:
            block = FoldScope.find_parent_scope(
                self.editor.document().findBlockByNumber(line))
            if TextBlockHelper.is_fold_trigger(block):
                if self._mouse_over_line is None:
                    # mouse enter fold scope
                    QtWidgets.QApplication.setOverrideCursor(
                        QtGui.QCursor(QtCore.Qt.PointingHandCursor))
                if self._mouse_over_line != block.blockNumber() and \
                        self._mouse_over_line is not None:
                    # fold scope changed, a previous block was highlighter so
                    # we quickly update our highlighting
                    self._mouse_over_line = block.blockNumber()
                    self._highlight_surrounding_scopes(block)
                else:
                    # same fold scope, request highlight
                    self._mouse_over_line = block.blockNumber()
                    self._highlight_runner.request_job(
                        self._highlight_surrounding_scopes, block)
                self._highight_block = block
            else:
                # no fold scope to highlight, cancel any pending requests
                self._highlight_runner.cancel_requests()
                self._mouse_over_line = None
                QtWidgets.QApplication.restoreOverrideCursor()
            self.repaint()
Ejemplo n.º 43
0
def test_matched_selection(editor):
    line, column, text = 233, 14, '''        editor.textCursor(), 'import', QtGui.QTextDocument.FindCaseSensitively'''
    cursor = editor.textCursor()
    assert not cursor.hasSelection()
    helper = TextHelper(editor)
    helper.goto_line(line, column)
    assert helper.cursor_position()[0] == line
    assert helper.cursor_position()[1] == column
    cursor = editor.textCursor()
    helper.match_select()
    cursor = editor.textCursor()
    assert cursor.hasSelection()
    assert text in cursor.selectedText()
Ejemplo n.º 44
0
def test_matched_selection(editor):
    line, column, text = 297, 14, '''__file__'''
    cursor = editor.textCursor()
    assert not cursor.hasSelection()
    helper = TextHelper(editor)
    helper.goto_line(line, column)
    assert helper.cursor_position()[0] == line
    assert helper.cursor_position()[1] == column
    cursor = editor.textCursor()
    helper.match_select()
    cursor = editor.textCursor()
    assert cursor.hasSelection()
    assert text in cursor.selectedText()
Ejemplo n.º 45
0
def test_goto_line(editor):
    assert editor.textCursor().blockNumber() == 0
    assert editor.textCursor().columnNumber() == 0
    cursor = TextHelper(editor).goto_line(2, 0, move=False)
    QTest.qWait(100)
    assert editor.textCursor().blockNumber() != cursor.blockNumber()
    assert editor.textCursor().columnNumber() == cursor.columnNumber()
    cursor = TextHelper(editor).goto_line(9, move=True)
    QTest.qWait(100)
    assert editor.textCursor().blockNumber() == cursor.blockNumber() == 9
    assert editor.textCursor().columnNumber() == cursor.columnNumber() == 0
    assert TextHelper(editor).current_line_nbr() == 9
    assert TextHelper(editor).current_column_nbr() == 0
Ejemplo n.º 46
0
def test_extended_selection(editor):
    for line, column, text in [(8, 15, 'pyqode.core.api.utils'),
                               (8, 1, 'from')]:
        editor.file.open(__file__)
        QTest.qWait(1000)
        cursor = editor.textCursor()
        assert not cursor.hasSelection()
        helper = TextHelper(editor)
        helper.goto_line(line, column)
        assert helper.cursor_position()[0] == line
        assert helper.cursor_position()[1] == column
        cursor = editor.textCursor()
        assert text in cursor.block().text()
        helper.select_extended_word()
        cursor = editor.textCursor()
        assert cursor.hasSelection()
        assert cursor.selectedText() == text
class CodeCompletionMode(Mode, QtCore.QObject):
    """ Provides code completions when typing or when pressing Ctrl+Space.

    This mode provides a code completion system which is extensible. µ
    It takes care of running the completion request in a background process
    using one or more completion provider.

    To implement a code completion for a specific language, you only need to
    implement new
    :class:`pyqode.core.backend.workers.CodeCompletionWorker.Provider`

    The completion popup is shown the user press **ctrl+space** or
    automatically while the user is typing some code (this can be configured
    using a series of properties).

    """
    @property
    def trigger_key(self):
        """
        The key that triggers code completion (Default is **Space**:
        Ctrl + Space).
        """
        return self._trigger_key

    @trigger_key.setter
    def trigger_key(self, value):
        self._trigger_key = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).trigger_key = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def trigger_length(self):
        """
        The trigger length defines the word length required to run code
        completion.
        """
        return self._trigger_len

    @trigger_length.setter
    def trigger_length(self, value):
        self._trigger_len = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).trigger_length = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def trigger_symbols(self):
        """
        Defines the list of symbols that immediately trigger a code completion
        requiest. BY default, this list contains the dot character.

        For C++, we would add the '->' operator to that list.
        """
        return self._trigger_symbols

    @trigger_symbols.setter
    def trigger_symbols(self, value):
        self._trigger_symbols = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).trigger_symbols = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def show_tooltips(self):
        """
        True to show tooltips next to the current completion.
        """
        return self._show_tooltips

    @show_tooltips.setter
    def show_tooltips(self, value):
        self._show_tooltips = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).show_tooltips = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def case_sensitive(self):
        """
        True to performs case sensitive completion matching.
        """
        return self._case_sensitive

    @case_sensitive.setter
    def case_sensitive(self, value):
        self._case_sensitive = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).case_sensitive = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def completion_prefix(self):
        """
        Returns the current completion prefix
        """
        return self._helper.word_under_cursor(
            select_whole_word=False).selectedText().strip()

    def __init__(self):
        Mode.__init__(self)
        QtCore.QObject.__init__(self)
        self._current_completion = ""
        # use to display a waiting cursor if completion provider takes too much
        # time
        self._job_runner = DelayJobRunner(delay=1000)
        self._tooltips = {}
        self._cursor_line = -1
        self._cancel_next = False
        self._request_cnt = 0
        self._last_completion_prefix = ""
        self._trigger_key = None
        self._trigger_len = None
        self._trigger_symbols = None
        self._show_tooltips = None
        self._case_sensitive = None
        self._data = None
        self._completer = None
        self._col = 0
        self._skip_next_backspace_released = False
        self._init_settings()

    def _init_settings(self):
        self._trigger_key = QtCore.Qt.Key_Space
        self._trigger_len = 1
        self._trigger_symbols = ['.']
        self._show_tooltips = True
        self._case_sensitive = False

    def request_completion(self):
        """
        Requests a code completion at the current cursor position.
        """
        _logger().debug('request code completion')
        self._col = self.editor.textCursor().positionInBlock() - len(
            self.completion_prefix)
        helper = TextHelper(self.editor)
        if not self._request_cnt:
            # only check first byte
            tc = self.editor.textCursor()
            while tc.atBlockEnd() and not tc.atBlockStart() and \
                    tc.position():
                tc.movePosition(tc.Left)
            disabled_zone = TextHelper(self.editor).is_comment_or_string(
                tc)
            if disabled_zone:
                _logger().debug(
                    "cc: cancel request, cursor is in a disabled zone")
                return False
            self._request_cnt += 1
            self._collect_completions(self.editor.toPlainText(),
                                      helper.current_line_nbr(),
                                      helper.current_column_nbr() -
                                      len(self.completion_prefix),
                                      self.editor.file.path,
                                      self.editor.file.encoding,
                                      self.completion_prefix)
            return True
        return False

    def on_install(self, editor):
        self._completer = QtWidgets.QCompleter([""], editor)
        self._completer.setCompletionMode(self._completer.PopupCompletion)
        self._completer.activated.connect(self._insert_completion)
        self._completer.highlighted.connect(
            self._on_selected_completion_changed)
        self._completer.setModel(QtGui.QStandardItemModel())
        self._helper = TextHelper(editor)
        Mode.on_install(self, editor)

    def on_uninstall(self):
        Mode.on_uninstall(self)
        self._completer.popup().hide()
        self._completer = None

    def on_state_changed(self, state):
        if state:
            self.editor.focused_in.connect(self._on_focus_in)
            self.editor.key_pressed.connect(self._on_key_pressed)
            self.editor.post_key_pressed.connect(self._on_key_released)
            self._completer.highlighted.connect(
                self._display_completion_tooltip)
            self.editor.cursorPositionChanged.connect(
                self._on_cursor_position_changed)
        else:
            self.editor.focused_in.disconnect(self._on_focus_in)
            self.editor.key_pressed.disconnect(self._on_key_pressed)
            self.editor.post_key_pressed.disconnect(self._on_key_released)
            self._completer.highlighted.disconnect(
                self._display_completion_tooltip)
            self.editor.cursorPositionChanged.disconnect(
                self._on_cursor_position_changed)

    def _on_focus_in(self, event):
        """
        Resets completer widget

        :param event: QFocusEvents
        """
        self._completer.setWidget(self.editor)

    def _on_results_available(self, results):
        _logger().debug("cc: got completion results")
        if self.editor:
            # self.editor.set_mouse_cursor(QtCore.Qt.IBeamCursor)
            all_results = []
            for res in results:
                all_results += res
            self._request_cnt -= 1
            self._show_completions(all_results)

    def _on_key_pressed(self, event):
        is_shortcut = self._is_shortcut(event)
        # handle completer popup events ourselves
        if self._completer.popup().isVisible():
            self._handle_completer_events(event)
            if is_shortcut:
                event.accept()
        if is_shortcut:
            self.request_completion()
            event.accept()

    @staticmethod
    def _is_navigation_key(event):
        return (event.key() == QtCore.Qt.Key_Backspace or
                event.key() == QtCore.Qt.Key_Back or
                event.key() == QtCore.Qt.Key_Delete or
                event.key() == QtCore.Qt.Key_Left or
                event.key() == QtCore.Qt.Key_Right or
                event.key() == QtCore.Qt.Key_Up or
                event.key() == QtCore.Qt.Key_Down or
                event.key() == QtCore.Qt.Key_Space)

    @staticmethod
    def _is_end_of_word_char(event, is_printable, symbols, seps):
        ret_val = False
        if is_printable and symbols:
            k = event.text()
            ret_val = (k in seps and k not in symbols)
        return ret_val

    def _update_prefix(self, event, is_end_of_word, is_navigation_key):
        self._completer.setCompletionPrefix(self.completion_prefix)
        cnt = self._completer.completionCount()
        n = len(self.editor.textCursor().block().text())
        c = self.editor.textCursor().positionInBlock()
        if (not cnt or ((self.completion_prefix == "" and n == 0) and
                        is_navigation_key) or
                is_end_of_word or
                c < self._col or
                (int(event.modifiers()) and
                 event.key() == QtCore.Qt.Key_Backspace)):
            self._hide_popup()
        else:
            self._show_popup()

    def _on_key_released(self, event):
        if (event.key() == QtCore.Qt.Key_Backspace and
                self._skip_next_backspace_released):
            self._skip_next_backspace_released = False
            return
        if self._is_shortcut(event):
            return
        if (event.key() == QtCore.Qt.Key_Home or
                event.key() == QtCore.Qt.Key_End or
                event.key() == QtCore.Qt.Key_Shift):
            return
        is_printable = self._is_printable_key_event(event)
        is_navigation_key = self._is_navigation_key(event)
        symbols = self._trigger_symbols
        is_end_of_word = self._is_end_of_word_char(
            event, is_printable, symbols, self.editor.word_separators)
        cursor = self._helper.word_under_cursor()
        cpos = cursor.position()
        cursor.setPosition(cpos)
        cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
        text_to_cursor = cursor.selectedText()
        cursor.setPosition(cpos)
        cursor.movePosition(cursor.EndOfLine, cursor.KeepAnchor)
        text_after_cursor = cursor.selectedText()

        if self._completer.popup().isVisible():
            # hide popup if the user is moving the cursor out of the current
            # word boundaries.
            if (len(text_after_cursor) and is_navigation_key and
                    text_after_cursor[0] in self.editor.word_separators and
                    not text_after_cursor[0].isspace()):
                self._hide_popup()
            else:
                self._update_prefix(event, is_end_of_word, is_navigation_key)
        if is_printable:
            if event.text() == " ":
                self._cancel_next = self._request_cnt
                return
            else:
                # trigger symbols
                if symbols:
                    for symbol in symbols:
                        if text_to_cursor.endswith(symbol):
                            _logger().debug("cc: symbols trigger")
                            self._hide_popup()
                            self.request_completion()
                            return
                # trigger length
                if (not self._completer.popup().isVisible() and
                        event.text().isalnum()):
                    prefix_len = len(self.completion_prefix)
                    if prefix_len >= self._trigger_len:
                        _logger().debug("cc: Len trigger")
                        self.request_completion()
                        return
            if self.completion_prefix == "":
                return self._hide_popup()

    def _on_selected_completion_changed(self, completion):
        self._current_completion = completion

    def _on_cursor_position_changed(self):
        current_line = TextHelper(self.editor).current_line_nbr()
        if current_line != self._cursor_line:
            self._cursor_line = current_line
            self._hide_popup()
            self._job_runner.cancel_requests()

    @QtCore.Slot()
    def _set_wait_cursor(self):
        # self.editor.set_mouse_cursor(QtCore.Qt.WaitCursor)
        pass

    def _is_last_char_end_of_word(self):
        try:
            cursor = self._helper.word_under_cursor()
            cursor.setPosition(cursor.position())
            cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
            line = cursor.selectedText()
            last_char = line[len(line) - 1]
            if last_char != ' ':
                symbols = self._trigger_symbols
                seps = self.editor.word_separators
                return last_char in seps and last_char not in symbols
            return False
        except (IndexError, TypeError):
            return False

    def _show_completions(self, completions):
        _logger().info("showing %d completions" % len(completions))
        self._job_runner.cancel_requests()
        # user typed too fast: end of word char has been inserted
        if (not self._cancel_next and not self._is_last_char_end_of_word() and
                not (len(completions) == 1 and
                     completions[0]['name'] == self.completion_prefix)):
            # we can show the completer
            t = time.time()
            self._update_model(completions)
            elapsed = time.time() - t
            _logger().info("completion model updated: %d items in %f seconds",
                            self._completer.model().rowCount(), elapsed)
            self._show_popup()
            _logger().info("popup shown")
        self._cancel_next = False

    def _handle_completer_events(self, event):
        nav_key = self._is_navigation_key(event)
        mod = QtCore.Qt.ControlModifier
        ctrl = int(event.modifiers() & mod) == mod
        # complete
        if (event.key() == QtCore.Qt.Key_Enter or
                event.key() == QtCore.Qt.Key_Return or
                event.key() == QtCore.Qt.Key_Tab):
            self._insert_completion(self._current_completion)
            self._hide_popup()
            event.accept()
        # hide
        elif (event.key() == QtCore.Qt.Key_Escape or
              event.key() == QtCore.Qt.Key_Backtab or
              nav_key and ctrl):
            self._hide_popup()
        # move into list
        elif event.key() == QtCore.Qt.Key_Home:
            self._show_popup(index=0)
            event.accept()
        elif event.key() == QtCore.Qt.Key_End:
            self._show_popup(index=self._completer.completionCount() - 1)
            event.accept()

    def _hide_popup(self):
        if (self._completer.popup() is not None and
                self._completer.popup().isVisible()):
            self._completer.popup().hide()
            QtWidgets.QToolTip.hideText()
        self._job_runner.cancel_requests()

    def _show_popup(self, index=0):
        full_prefix = self._helper.word_under_cursor(
            select_whole_word=False).selectedText()
        if self._case_sensitive:
                self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive)
        else:
            self._completer.setCaseSensitivity(
                QtCore.Qt.CaseInsensitive)
        # set prefix
        self._completer.setCompletionPrefix(self.completion_prefix)
        cnt = self._completer.completionCount()
        selected = self._completer.currentCompletion()
        if (full_prefix == selected) and cnt == 1:
            self._hide_popup()
        else:
            # compute size and pos
            cursor_rec = self.editor.cursorRect()
            char_width = self.editor.fontMetrics().width('A')
            prefix_len = (len(self.completion_prefix) * char_width)
            cursor_rec.translate(self.editor.panels.margin_size() - prefix_len,
                                 # top
                                 self.editor.panels.margin_size(0) + 5)
            popup = self._completer.popup()
            width = popup.verticalScrollBar().sizeHint().width()
            cursor_rec.setWidth(self._completer.popup().sizeHintForColumn(0) +
                                width)
            # show the completion list
            if self.editor.isVisible():
                if not self._completer.popup().isVisible():
                    self._on_focus_in(None)
                self._completer.complete(cursor_rec)
                self._completer.popup().setCurrentIndex(
                    self._completer.completionModel().index(index, 0))
            else:
                _logger().debug('cannot show popup, editor is unvisible')

    def _insert_completion(self, completion):
        cursor = self._helper.word_under_cursor(select_whole_word=False)
        cursor.insertText(completion)
        self.editor.setTextCursor(cursor)

    def _is_shortcut(self, event):
        """
        Checks if the event's key and modifiers make the completion shortcut
        (Ctrl+Space)

        :param event: QKeyEvent

        :return: bool
        """
        modifier = (QtCore.Qt.MetaModifier if sys.platform == 'darwin' else
                    QtCore.Qt.ControlModifier)
        valid_modifier = int(event.modifiers() & modifier) == modifier
        valid_key = event.key() == self._trigger_key
        _logger().debug("CC: Valid Mofifier: %r, Valid Key: %r" %
                        (valid_modifier, valid_key))
        return valid_key and valid_modifier

    @staticmethod
    def strip_control_characters(input_txt):
        """
        Strips control character from ``input_txt``
        :param input_txt: text to strip.
        :return: stripped text
        """
        if input_txt:
            # unicode invalid characters
            re_illegal = \
                '([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \
                '|' + \
                '([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \
                (chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),
                 chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),
                 chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff))
            input_txt = re.sub(re_illegal, "", input_txt)
            # ascii control characters
            input_txt = re.sub(r"[\x01-\x1F\x7F]", "", input_txt)
        return input_txt

    def _is_printable_key_event(self, event):
        return (event.text().isalnum() or
                event.text() in self.editor.word_separators)

    @staticmethod
    @memoized
    def _make_icon(icon):
        return QtGui.QIcon(icon)

    def _update_model(self, completions):
        """
        Creates a QStandardModel that holds the suggestion from the completion
        models for the QCompleter

        :param completionPrefix:
        """
        # build the completion model
        cc_model = QtGui.QStandardItemModel()
        self._tooltips.clear()
        for completion in completions:
            name = completion['name']
            # skip redundant completion
            # if (name and name != self.completion_prefix and
            #         name not in displayed_texts):
            item = QtGui.QStandardItem()
            item.setData(name, QtCore.Qt.DisplayRole)
            if 'tooltip' in completion and completion['tooltip']:
                self._tooltips[name] = completion['tooltip']
            if 'icon' in completion:
                item.setData(self._make_icon(completion['icon']),
                             QtCore.Qt.DecorationRole)
            cc_model.appendRow(item)
        self._completer.setModel(cc_model)
        return cc_model

    def _display_completion_tooltip(self, completion):
        if not self._show_tooltips:
            return
        if completion not in self._tooltips:
            QtWidgets.QToolTip.hideText()
            return
        tooltip = self._tooltips[completion].strip()
        pos = self._completer.popup().pos()
        pos.setX(pos.x() + self._completer.popup().size().width())
        pos.setY(pos.y() - 15)
        QtWidgets.QToolTip.showText(pos, tooltip, self.editor)

    def _collect_completions(self, code, line, column, path, encoding,
                             completion_prefix):
        _logger().debug("cc: completion requested")
        data = {'code': code, 'line': line, 'column': column,
                'path': path, 'encoding': encoding,
                'prefix': completion_prefix}
        try:
            self.editor.backend.send_request(
                backend.CodeCompletionWorker, args=data,
                on_receive=self._on_results_available)
        except NotRunning:
            self._data = data
            QtCore.QTimer.singleShot(100, self._retry_collect)
            _logger().debug('retry collect_completions in 100ms')
        else:
            self._set_wait_cursor()

    def _retry_collect(self):
        _logger().debug('retry work request')
        try:
            self.editor.backend.send_request(
                backend.CodeCompletionWorker, args=self._data,
                on_receive=self._on_results_available)
        except NotRunning:
            QtCore.QTimer.singleShot(100, self._retry_collect)
        else:
            self._set_wait_cursor()

    def clone_settings(self, original):
        self.trigger_key = original.trigger_key
        self.trigger_length = original.trigger_length
        self.trigger_symbols = original.trigger_symbols
        self.show_tooltips = original.show_tooltips
        self.case_sensitive = original.case_sensitive
Ejemplo n.º 48
0
def test_select_whole_line(editor):
    cursor = TextHelper(editor).select_whole_line(2)
    assert isinstance(cursor, QtGui.QTextCursor)
    assert cursor.hasSelection()
    assert cursor.selectionStart() == 28
    assert cursor.selectionEnd() == 99
Ejemplo n.º 49
0
 def on_install(self, editor):
     super(SearchAndReplacePanel, self).on_install(editor)
     self.hide()
     self.text_helper = TextHelper(editor)
Ejemplo n.º 50
0
class SearchAndReplacePanel(Panel, Ui_SearchPanel):
    """ Lets the user search and replace text in the current document.

    It uses the backend API to search for some text. Search operation is
    performed in a background process (the backend process)..

    The search panel can also be used programatically. To do that, the client
    code must first requests a search using :meth:`requestSearch` and connects
    to :attr:`search_finished`. The results of the search can then be
    retrieved using :attr:`cpt_occurences` and :meth:`get_oOccurrences`. The
    client code may now navigate through occurrences using :meth:`select_next`
    or :meth:`select_previous`, or replace the occurrences with a specific
    text using :meth:`replace` or :meth:`replace_all`.
    """
    STYLESHEET = """SearchAndReplacePanel
    {
        background-color: %(bck)s;
        color: %(color)s;
    }

    QtoolButton
    {
        color: %(color)s;
        background-color: transparent;
        padding: 5px;
        min-height: 24px;
        min-width: 24px;
        border: none;
    }

    QtoolButton:hover
    {
        background-color: %(highlight)s;
        border: none;
        border-radius: 5px;
        color: %(color)s;
    }

    QtoolButton:pressed, QCheckBox:pressed
    {
        border: 1px solid %(bck)s;
    }

    QtoolButton:disabled
    {
        color: %(highlight)s;
    }

    QCheckBox
    {
        padding: 4px;
        color: %(color)s;
    }

    QCheckBox:hover
    {
            background-color: %(highlight)s;
            color: %(color)s;
            border-radius: 5px;
    }
    """
    _KEYS = ["panelBackground", "background", "panelForeground",
             "panelHighlight"]

    #: Signal emitted when a search operation finished
    search_finished = QtCore.Signal()

    #: Define the maximum number of occurences that can be highlighted
    #: in the document.
    #:
    #: .. note:: The search operation itself is fast but the creation of all
    #:    the extra selection used to highlight search result can be slow.
    MAX_HIGHLIGHTED_OCCURENCES = 500

    @property
    def background(self):
        """ Text decoration background """
        return self._bg

    @background.setter
    def background(self, value):
        self._bg = value
        self._refresh_decorations()
        # propagate changes to every clone
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).background = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def foreground(self):
        """ Text decoration foreground """
        return self._fg

    @foreground.setter
    def foreground(self, value):
        self._fg = value
        self._refresh_decorations()
        # propagate changes to every clone
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).foreground = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    def __init__(self):
        Panel.__init__(self, dynamic=True)
        self.job_runner = DelayJobRunner(delay=500)
        Ui_SearchPanel.__init__(self)
        self.setupUi(self)
        self.lineEditReplace.prompt_text = ' Replace'
        #: Occurrences counter
        self.cpt_occurences = 0
        self._previous_stylesheet = ""
        self._separator = None
        self._decorations = []
        self._occurrences = []
        self._current_occurrence_index = 0
        self._bg = None
        self._fg = None
        self._update_buttons(txt="")
        self.lineEditSearch.installEventFilter(self)
        self.lineEditReplace.installEventFilter(self)
        self._init_actions()
        self._init_style()
        self.checkBoxRegex.stateChanged.connect(
            self.checkBoxWholeWords.setDisabled)

    def _init_actions(self):
        icon_size = QtCore.QSize(16, 16)

        icon = icons.icon('edit-find', ':/pyqode-icons/rc/edit-find.png',
                          'fa.search')
        self.actionSearch.setIcon(icon)
        self.actionSearch.setShortcut('Ctrl+F')
        self.labelSearch.setPixmap(icon.pixmap(icon_size))

        icon = icons.icon(
            'edit-find-replace', ':/pyqode-icons/rc/edit-find-replace.png',
            'fa.search-plus')
        self.actionActionSearchAndReplace.setShortcut(
            'Ctrl+H')
        self.actionActionSearchAndReplace.setIcon(icon)
        self.labelReplace.setPixmap(icon.pixmap(icon_size))

        icon = icons.icon('go-up', ':/pyqode-icons/rc/go-up.png',
                          'fa.arrow-up')
        self.actionFindPrevious.setShortcut('Shift+F3')
        self.actionFindPrevious.setIcon(icon)
        self.toolButtonPrevious.setIcon(icon)
        self.toolButtonPrevious.setIconSize(icon_size)

        icon = icons.icon('go-down', ':/pyqode-icons/rc/go-down.png',
                          'fa.arrow-down')
        self.actionFindNext.setShortcut('F3')
        self.actionFindNext.setIcon(icon)
        self.toolButtonNext.setIcon(icon)
        self.toolButtonNext.setIconSize(icon_size)

        icon = icons.icon('window-close', ':/pyqode-icons/rc/close.png',
                          'fa.close')
        self.toolButtonClose.setIcon(icon)
        self.toolButtonClose.setIconSize(icon_size)

        self.menu = QtWidgets.QMenu(self.editor)
        self.menu.setTitle('Search')
        self.menu.menuAction().setIcon(self.actionSearch.icon())
        self.menu.addAction(self.actionSearch)
        self.actionSearch.setShortcutContext(QtCore.Qt.WidgetShortcut)
        self.menu.addAction(self.actionActionSearchAndReplace)
        self.actionActionSearchAndReplace.setShortcutContext(
            QtCore.Qt.WidgetShortcut)
        self.menu.addAction(self.actionFindNext)
        self.actionFindNext.setShortcutContext(
            QtCore.Qt.WidgetShortcut)
        self.menu.addAction(self.actionFindPrevious)
        self.actionFindPrevious.setShortcutContext(
            QtCore.Qt.WidgetShortcut)

    def _init_style(self):
        self._bg = QtGui.QColor('yellow')
        self._outline = QtGui.QPen(QtGui.QColor('gray'), 1)

    def on_install(self, editor):
        super(SearchAndReplacePanel, self).on_install(editor)
        self.hide()
        self.text_helper = TextHelper(editor)

    def _refresh_decorations(self):
        for deco in self._decorations:
            self.editor.decorations.remove(deco)
            deco.set_background(QtGui.QBrush(self.background))
            deco.set_outline(self._outline)
            self.editor.decorations.append(deco)

    def on_state_changed(self, state):
        super(SearchAndReplacePanel, self).on_state_changed(state)
        if state:
            # menu
            self.editor.add_action(self.menu.menuAction())
            # requestSearch slot
            self.editor.textChanged.connect(self.request_search)
            self.lineEditSearch.textChanged.connect(self.request_search)
            self.checkBoxCase.stateChanged.connect(self.request_search)
            self.checkBoxWholeWords.stateChanged.connect(self.request_search)
            self.checkBoxRegex.stateChanged.connect(self.request_search)
            self.checkBoxInSelection.stateChanged.connect(self.request_search)
            # navigation slots
            self.toolButtonNext.clicked.connect(self.select_next)
            self.actionFindNext.triggered.connect(self.select_next)
            self.toolButtonPrevious.clicked.connect(self.select_previous)
            self.actionFindPrevious.triggered.connect(self.select_previous)
            # replace slots
            self.toolButtonReplace.clicked.connect(self.replace)
            self.toolButtonReplaceAll.clicked.connect(self.replace_all)
            # internal updates slots
            self.lineEditReplace.textChanged.connect(self._update_buttons)
            self.search_finished.connect(self._on_search_finished)
        else:
            self.editor.remove_action(self.menu.menuAction())
            # requestSearch slot
            self.editor.textChanged.disconnect(self.request_search)
            self.lineEditSearch.textChanged.disconnect(self.request_search)
            self.checkBoxCase.stateChanged.disconnect(self.request_search)
            self.checkBoxWholeWords.stateChanged.disconnect(
                self.request_search)
            self.checkBoxRegex.stateChanged.disconnect(self.request_search)
            self.checkBoxInSelection.stateChanged.disconnect(
                self.request_search)
            # navigation slots
            self.toolButtonNext.clicked.disconnect(self.select_next)
            self.actionFindNext.triggered.disconnect(self.select_next)
            self.toolButtonPrevious.clicked.disconnect(self.select_previous)
            # replace slots
            self.toolButtonReplace.clicked.disconnect(self.replace)
            self.toolButtonReplaceAll.clicked.disconnect(self.replace_all)
            # internal updates slots
            self.lineEditReplace.textChanged.disconnect(self._update_buttons)
            self.search_finished.disconnect(self._on_search_finished)

    def close_panel(self):
        """
        Closes the panel
        """
        self.hide()
        self.lineEditReplace.clear()
        self.lineEditSearch.clear()

    @QtCore.Slot()
    def on_toolButtonClose_clicked(self):
        self.close_panel()

    @QtCore.Slot()
    def on_actionSearch_triggered(self):
        self.widgetSearch.show()
        self.widgetReplace.hide()
        self.show()
        new_text = self.text_helper.selected_text()
        old_text = self.lineEditSearch.text()
        text_changed = new_text != old_text
        self.lineEditSearch.setText(new_text)
        self.lineEditSearch.selectAll()
        self.lineEditSearch.setFocus()
        self.setFocusPolicy(QtCore.Qt.ClickFocus)
        if not text_changed:
            self.request_search(new_text)

    @QtCore.Slot()
    def on_actionActionSearchAndReplace_triggered(self):
        self.widgetSearch.show()
        self.widgetReplace.show()
        self.show()
        new_txt = self.text_helper.selected_text()
        old_txt = self.lineEditSearch.text()
        txt_changed = new_txt != old_txt
        self.lineEditSearch.setText(new_txt)
        self.lineEditReplace.clear()
        self.lineEditReplace.setFocus()
        if not txt_changed:
            self.request_search(new_txt)

    def focusOutEvent(self, event):
        self.job_runner.cancel_requests()
        Panel.focusOutEvent(self, event)

    def request_search(self, txt=None):
        """
        Requests a search operation.

        :param txt: The text to replace. If None, the content of lineEditSearch
                    is used instead.
        """
        if self.checkBoxRegex.isChecked():
            try:
                re.compile(self.lineEditSearch.text(), re.DOTALL)
            except sre_constants.error as e:
                self._show_error(e)
                return
            else:
                self._show_error(None)

        if txt is None or isinstance(txt, int):
            txt = self.lineEditSearch.text()
        if txt:
            self.job_runner.request_job(
                self._exec_search, txt, self._search_flags())
        else:
            self.job_runner.cancel_requests()
            self._clear_occurrences()
            self._on_search_finished()

    @staticmethod
    def _set_widget_background_color(widget, color):
        """
        Changes the base color of a widget (background).
        :param widget: widget to modify
        :param color: the color to apply
        """
        pal = widget.palette()
        pal.setColor(pal.Base, color)
        widget.setPalette(pal)

    def _show_error(self, error):
        if error:
            self._set_widget_background_color(
                self.lineEditSearch, QtGui.QColor('#FFCCCC'))
            self.lineEditSearch.setToolTip(str(error))
        else:
            self._set_widget_background_color(
                self.lineEditSearch, self.palette().color(
                    self.palette().Base))
            self.lineEditSearch.setToolTip('')

    def get_occurences(self):
        """
        Returns the list of text occurrences.

        An occurrence is a tuple that contains start and end positions.

        :return: List of tuple(int, int)
        """
        return self._occurrences

    def select_next(self):
        """
        Selects the next occurrence.

        :return: True in case of success, false if no occurrence could be
                 selected.
        """
        current_occurence = self._current_occurrence()
        occurrences = self.get_occurences()
        if not occurrences:
            return
        current = self._occurrences[current_occurence]
        cursor_pos = self.editor.textCursor().position()
        if cursor_pos not in range(current[0], current[1] + 1) or \
                current_occurence == -1:
            # search first occurrence that occurs after the cursor position
            current_occurence = 0
            for i, (start, end) in enumerate(self._occurrences):
                if end > cursor_pos:
                    current_occurence = i
                    break
        else:
            if (current_occurence == -1 or
                    current_occurence >= len(occurrences) - 1):
                current_occurence = 0
            else:
                current_occurence += 1
        self._set_current_occurrence(current_occurence)
        try:
            cursor = self.editor.textCursor()
            cursor.setPosition(occurrences[current_occurence][0])
            cursor.setPosition(occurrences[current_occurence][1],
                               cursor.KeepAnchor)
            self.editor.setTextCursor(cursor)
            return True
        except IndexError:
            return False

    def select_previous(self):
        """
        Selects previous occurrence.

        :return: True in case of success, false if no occurrence could be
                 selected.
        """
        current_occurence = self._current_occurrence()
        occurrences = self.get_occurences()
        if not occurrences:
            return
        current = self._occurrences[current_occurence]
        cursor_pos = self.editor.textCursor().position()
        if cursor_pos not in range(current[0], current[1] + 1) or \
                current_occurence == -1:
            # search first occurrence that occurs before the cursor position
            current_occurence = len(self._occurrences) - 1
            for i, (start, end) in enumerate(self._occurrences):
                if end >= cursor_pos:
                    current_occurence = i - 1
                    break
        else:
            if (current_occurence == -1 or
                    current_occurence == 0):
                current_occurence = len(occurrences) - 1
            else:
                current_occurence -= 1
        self._set_current_occurrence(current_occurence)
        try:
            cursor = self.editor.textCursor()
            cursor.setPosition(occurrences[current_occurence][0])
            cursor.setPosition(occurrences[current_occurence][1],
                               cursor.KeepAnchor)
            self.editor.setTextCursor(cursor)
            return True
        except IndexError:
            return False

    def replace(self, text=None):
        """
        Replaces the selected occurrence.

        :param text: The replacement text. If it is None, the lineEditReplace's
                     text is used instead.

        :return True if the text could be replace properly, False if there is
                no more occurrences to replace.
        """
        if text is None or isinstance(text, bool):
            text = self.lineEditReplace.text()
        current_occurences = self._current_occurrence()
        occurrences = self.get_occurences()
        if current_occurences == -1:
            self.select_next()
            current_occurences = self._current_occurrence()
        try:
            # prevent search request due to editor textChanged
            try:
                self.editor.textChanged.disconnect(self.request_search)
            except (RuntimeError, TypeError):
                # already disconnected
                pass
            occ = occurrences[current_occurences]
            cursor = self.editor.textCursor()
            cursor.setPosition(occ[0])
            cursor.setPosition(occ[1], cursor.KeepAnchor)
            len_to_replace = len(cursor.selectedText())
            len_replacement = len(text)
            offset = len_replacement - len_to_replace
            cursor.insertText(text)
            self.editor.setTextCursor(cursor)
            self._remove_occurrence(current_occurences, offset)
            current_occurences -= 1
            self._set_current_occurrence(current_occurences)
            self.select_next()
            self.cpt_occurences = len(self.get_occurences())
            self._update_label_matches()
            self._update_buttons()
            return True
        except IndexError:
            return False
        finally:
            self.editor.textChanged.connect(self.request_search)

    def replace_all(self, text=None):
        """
        Replaces all occurrences in the editor's document.

        :param text: The replacement text. If None, the content of the lineEdit
                     replace will be used instead
        """
        cursor = self.editor.textCursor()
        cursor.beginEditBlock()
        remains = self.replace(text=text)
        while remains:
            remains = self.replace(text=text)
        cursor.endEditBlock()

    def eventFilter(self, obj, event):
        if event.type() == QtCore.QEvent.KeyPress:
            if (event.key() == QtCore.Qt.Key_Tab or
                    event.key() == QtCore.Qt.Key_Backtab):
                return True
            elif (event.key() == QtCore.Qt.Key_Return or
                  event.key() == QtCore.Qt.Key_Enter):
                if obj == self.lineEditReplace:
                    if event.modifiers() & QtCore.Qt.ControlModifier:
                        self.replace_all()
                    else:
                        self.replace()
                elif obj == self.lineEditSearch:
                    if event.modifiers() & QtCore.Qt.ShiftModifier:
                        self.select_previous()
                    else:
                        self.select_next()
                return True
            elif event.key() == QtCore.Qt.Key_Escape:
                self.on_toolButtonClose_clicked()
        return Panel.eventFilter(self, obj, event)

    def _search_flags(self):
        """
        Returns the user search flag: (regex, case_sensitive, whole_words).
        """
        return (self.checkBoxRegex.isChecked(),
                self.checkBoxCase.isChecked(),
                self.checkBoxWholeWords.isChecked(),
                self.checkBoxInSelection.isChecked())

    def _exec_search(self, sub, flags):
        if self.editor is None:
            return
        regex, case_sensitive, whole_word, in_selection = flags
        tc = self.editor.textCursor()
        assert isinstance(tc, QtGui.QTextCursor)
        if in_selection and tc.hasSelection():
            text = tc.selectedText()
            self._offset = tc.selectionStart()
        else:
            text = self.editor.toPlainText()
            self._offset = 0
        request_data = {
            'string': text,
            'sub': sub,
            'regex': regex,
            'whole_word': whole_word,
            'case_sensitive': case_sensitive
        }
        try:
            self.editor.backend.send_request(findall, request_data,
                                             self._on_results_available)
        except AttributeError:
            self._on_results_available(findall(request_data))

    def _on_results_available(self, results):
        self._occurrences = [(start + self._offset, end + self._offset)
                             for start, end in results]
        self._on_search_finished()

    def _update_label_matches(self):
        self.labelMatches.setText("{0} matches".format(self.cpt_occurences))
        color = "#DD0000"
        if self.cpt_occurences:
            color = "#00DD00"
        self.labelMatches.setStyleSheet("color: %s" % color)
        if self.lineEditSearch.text() == "":
            self.labelMatches.clear()

    def _on_search_finished(self):
        self._clear_decorations()
        all_occurences = self.get_occurences()
        occurrences = all_occurences[:self.MAX_HIGHLIGHTED_OCCURENCES]
        for i, occurrence in enumerate(occurrences):
            deco = self._create_decoration(occurrence[0],
                                           occurrence[1])
            self._decorations.append(deco)
            self.editor.decorations.append(deco)
        self.cpt_occurences = len(all_occurences)
        if not self.cpt_occurences:
            self._current_occurrence_index = -1
        else:
            self._current_occurrence_index = -1
        self._update_label_matches()
        self._update_buttons(txt=self.lineEditReplace.text())

    def _current_occurrence(self):
        ret_val = self._current_occurrence_index
        return ret_val

    def _clear_occurrences(self):
        self._occurrences[:] = []

    def _create_decoration(self, selection_start, selection_end):
        """ Creates the text occurences decoration """
        deco = TextDecoration(self.editor.document(), selection_start,
                              selection_end)
        deco.set_background(QtGui.QBrush(self.background))
        deco.set_outline(self._outline)
        deco.draw_order = 1
        return deco

    def _clear_decorations(self):
        """ Remove all decorations """
        for deco in self._decorations:
            self.editor.decorations.remove(deco)
        self._decorations[:] = []

    def _set_current_occurrence(self, current_occurence_index):
        self._current_occurrence_index = current_occurence_index

    def _remove_occurrence(self, i, offset=0):
        self._occurrences.pop(i)
        if offset:
            updated_occurences = []
            for j, occ in enumerate(self._occurrences):
                if j >= i:
                    updated_occurences.append(
                        (occ[0] + offset, occ[1] + offset))
                else:
                    updated_occurences.append((occ[0], occ[1]))
            self._occurrences = updated_occurences

    def _update_buttons(self, txt=""):
        enable = self.cpt_occurences > 1
        self.toolButtonNext.setEnabled(enable)
        self.toolButtonPrevious.setEnabled(enable)
        self.actionFindNext.setEnabled(enable)
        self.actionFindPrevious.setEnabled(enable)
        enable = (txt != self.lineEditSearch.text() and
                  self.cpt_occurences)
        self.toolButtonReplace.setEnabled(enable)
        self.toolButtonReplaceAll.setEnabled(enable)

    def clone_settings(self, original):
        self.background = original.background
        self.foreground = original.foreground
Ejemplo n.º 51
0
 def on_install(self, editor):
     self._create_completer()
     self._completer.setModel(QtGui.QStandardItemModel())
     self._helper = TextHelper(editor)
     Mode.on_install(self, editor)
Ejemplo n.º 52
0
class CodeCompletionMode(Mode, QtCore.QObject):
    """ Provides code completions when typing or when pressing Ctrl+Space.

    This mode provides a code completion system which is extensible.
    It takes care of running the completion request in a background process
    using one or more completion provider and display the results in a
    QCompleter.

    To add code completion for a specific language, you only need to
    implement a new
    :class:`pyqode.core.backend.workers.CodeCompletionWorker.Provider`

    The completion popup is shown when the user press **ctrl+space** or
    automatically while the user is typing some code (this can be configured
    using a series of properties).
    """
    #: Filter completions based on the prefix. FAST
    FILTER_PREFIX = 0
    #: Filter completions based on whether the prefix is contained in the
    #: suggestion. Only available with PyQt5, if set with PyQt4, FILTER_PREFIX
    #: will be used instead. FAST
    FILTER_CONTAINS = 1
    #: Fuzzy filtering, using the subsequence matcher. This is the most
    #: powerful filter mode but also the SLOWEST.
    FILTER_FUZZY = 2

    @property
    def filter_mode(self):
        """
        The completion filter mode
        """
        return self._filter_mode

    @filter_mode.setter
    def filter_mode(self, value):
        self._filter_mode = value
        self._create_completer()

    @property
    def trigger_key(self):
        """
        The key that triggers code completion (Default is **Space**:
        Ctrl + Space).
        """
        return self._trigger_key

    @trigger_key.setter
    def trigger_key(self, value):
        self._trigger_key = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).trigger_key = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def trigger_length(self):
        """
        The trigger length defines the word length required to run code
        completion.
        """
        return self._trigger_len

    @trigger_length.setter
    def trigger_length(self, value):
        self._trigger_len = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).trigger_length = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def trigger_symbols(self):
        """
        Defines the list of symbols that immediately trigger a code completion
        requiest. BY default, this list contains the dot character.

        For C++, we would add the '->' operator to that list.
        """
        return self._trigger_symbols

    @trigger_symbols.setter
    def trigger_symbols(self, value):
        self._trigger_symbols = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).trigger_symbols = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def case_sensitive(self):
        """
        True to performs case sensitive completion matching.
        """
        return self._case_sensitive

    @case_sensitive.setter
    def case_sensitive(self, value):
        self._case_sensitive = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).case_sensitive = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def completion_prefix(self):
        """
        Returns the current completion prefix
        """
        return self._helper.word_under_cursor(
            select_whole_word=False).selectedText().strip()

    @property
    def show_tooltips(self):
        """
        True to show tooltips next to the current completion.
        """
        return self._show_tooltips

    @show_tooltips.setter
    def show_tooltips(self, value):
        self._show_tooltips = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(CodeCompletionMode).show_tooltips = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    def __init__(self):
        Mode.__init__(self)
        QtCore.QObject.__init__(self)
        self._current_completion = ""
        self._trigger_key = QtCore.Qt.Key_Space
        self._trigger_len = 1
        self._trigger_symbols = ['.']
        self._case_sensitive = False
        self._completer = None
        self._filter_mode = self.FILTER_FUZZY
        self._last_cursor_line = -1
        self._last_cursor_column = -1
        self._tooltips = {}
        self._show_tooltips = False
        self._request_id = self._last_request_id = 0

    def clone_settings(self, original):
        self.trigger_key = original.trigger_key
        self.trigger_length = original.trigger_length
        self.trigger_symbols = original.trigger_symbols
        self.show_tooltips = original.show_tooltips
        self.case_sensitive = original.case_sensitive

    #
    # Mode interface
    #
    def _create_completer(self):
        if self.filter_mode != self.FILTER_FUZZY:
            self._completer = QtWidgets.QCompleter([''], self.editor)
            if self.filter_mode == self.FILTER_CONTAINS:
                try:
                    self._completer.setFilterMode(QtCore.Qt.MatchContains)
                except AttributeError:
                    # only available with PyQt5
                    pass
        else:
            self._completer = SubsequenceCompleter(self.editor)
        self._completer.setCompletionMode(self._completer.PopupCompletion)
        if self.case_sensitive:
            self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive)
        else:
            self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self._completer.activated.connect(self._insert_completion)
        self._completer.highlighted.connect(
            self._on_selected_completion_changed)
        self._completer.highlighted.connect(self._display_completion_tooltip)

    def on_install(self, editor):
        self._create_completer()
        self._completer.setModel(QtGui.QStandardItemModel())
        self._helper = TextHelper(editor)
        Mode.on_install(self, editor)

    def on_uninstall(self):
        Mode.on_uninstall(self)
        self._completer.popup().hide()
        self._completer = None

    def on_state_changed(self, state):
        if state:
            self.editor.focused_in.connect(self._on_focus_in)
            self.editor.key_pressed.connect(self._on_key_pressed)
            self.editor.post_key_pressed.connect(self._on_key_released)
        else:
            self.editor.focused_in.disconnect(self._on_focus_in)
            self.editor.key_pressed.disconnect(self._on_key_pressed)
            self.editor.post_key_pressed.disconnect(self._on_key_released)

    #
    # Slots
    #
    def _on_key_pressed(self, event):
        def _handle_completer_events():
            nav_key = self._is_navigation_key(event)
            mod = QtCore.Qt.ControlModifier
            ctrl = int(event.modifiers() & mod) == mod
            # complete
            if event.key() in [
                    QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
                    QtCore.Qt.Key_Tab]:
                self._insert_completion(self._current_completion)
                self._hide_popup()
                event.accept()
            # hide
            elif (event.key() in [
                    QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backtab] or
                    nav_key and ctrl):
                self._reset_sync_data()
            # move into list
            elif event.key() == QtCore.Qt.Key_Home:
                self._show_popup(index=0)
                event.accept()
            elif event.key() == QtCore.Qt.Key_End:
                self._show_popup(index=self._completer.completionCount() - 1)
                event.accept()

        debug('key pressed: %s' % event.text())
        is_shortcut = self._is_shortcut(event)
        # handle completer popup events ourselves
        if self._completer.popup().isVisible():
            if is_shortcut:
                event.accept()
            else:
                _handle_completer_events()
        elif is_shortcut:
            self._reset_sync_data()
            self.request_completion()
            event.accept()

    def _on_key_released(self, event):
        if self._is_shortcut(event) or event.isAccepted():
            return
        debug('key released:%s' % event.text())
        word = self._helper.word_under_cursor(
            select_whole_word=True).selectedText()
        debug('word: %s' % word)
        if event.text():
            if event.key() == QtCore.Qt.Key_Escape:
                self._hide_popup()
                return
            if self._is_navigation_key(event) and \
                    (not self._is_popup_visible() or word == ''):
                self._reset_sync_data()
                return
            if event.key() == QtCore.Qt.Key_Return:
                return
            if event.text() in self._trigger_symbols:
                # symbol trigger, force request
                self._reset_sync_data()
                self.request_completion()
            elif len(word) >= self._trigger_len and event.text() not in \
                    self.editor.word_separators:
                # Length trigger
                if int(event.modifiers()) in [
                        QtCore.Qt.NoModifier, QtCore.Qt.ShiftModifier]:
                    self.request_completion()
                else:
                    self._hide_popup()
            else:
                self._reset_sync_data()
        else:
            if self._is_navigation_key(event):
                if self._is_popup_visible() and word:
                    self._show_popup()
                    return
                else:
                    self._reset_sync_data()

    def _on_focus_in(self, event):
        """
        Resets completer's widget

        :param event: QFocusEvents
        """
        self._completer.setWidget(self.editor)

    def _on_selected_completion_changed(self, completion):
        self._current_completion = completion

    def _insert_completion(self, completion):
        cursor = self._helper.word_under_cursor(select_whole_word=False)
        cursor.insertText(completion)
        self.editor.setTextCursor(cursor)

    def _on_results_available(self, results):
        debug("completion results (completions=%r), prefix=%s",
                        results, self.completion_prefix)
        context = results[0]
        results = results[1:]
        line, column, request_id = context
        debug('request context: %r', context)
        debug('latest context: %r', (self._last_cursor_line,
                                               self._last_cursor_column,
                                               self._request_id))
        self._last_request_id = request_id
        if (line == self._last_cursor_line and
                column == self._last_cursor_column):
            if self.editor:
                all_results = []
                for res in results:
                    all_results += res
                self._show_completions(all_results)
        else:
            debug('outdated request, dropping')

    #
    # Helper methods
    #
    def _is_popup_visible(self):
        return self._completer.popup().isVisible()

    def _reset_sync_data(self):
        debug('reset sync data and hide popup')
        self._last_cursor_line = -1
        self._last_cursor_column = -1
        self._hide_popup()

    def request_completion(self):
        line = self._helper.current_line_nbr()
        column = self._helper.current_column_nbr() - \
            len(self.completion_prefix)
        same_context = (line == self._last_cursor_line and
                        column == self._last_cursor_column)
        if same_context:
            if self._request_id - 1 == self._last_request_id:
                # context has not changed and the correct results can be
                # directly shown
                debug('request completion ignored, context has not '
                                'changed')
                self._show_popup()
            else:
                # same context but result not yet available
                pass
            return True
        else:
            debug('requesting completion')
            data = {
                'code': self.editor.toPlainText(),
                'line': line,
                'column': column,
                'path': self.editor.file.path,
                'encoding': self.editor.file.encoding,
                'prefix': self.completion_prefix,
                'request_id': self._request_id
            }
            try:
                self.editor.backend.send_request(
                    backend.CodeCompletionWorker, args=data,
                    on_receive=self._on_results_available)
            except NotRunning:
                _logger().exception('failed to send the completion request')
                return False
            else:
                debug('request sent: %r', data)
                self._last_cursor_column = column
                self._last_cursor_line = line
                self._request_id += 1
                return True

    def _is_shortcut(self, event):
        """
        Checks if the event's key and modifiers make the completion shortcut
        (Ctrl+Space)

        :param event: QKeyEvent

        :return: bool
        """
        modifier = (QtCore.Qt.MetaModifier if sys.platform == 'darwin' else
                    QtCore.Qt.ControlModifier)
        valid_modifier = int(event.modifiers() & modifier) == modifier
        valid_key = event.key() == self._trigger_key
        return valid_key and valid_modifier

    def _hide_popup(self):
        """
        Hides the completer popup
        """
        debug('hide popup')
        if (self._completer.popup() is not None and
                self._completer.popup().isVisible()):
            self._completer.popup().hide()
            self._last_cursor_column = -1
            self._last_cursor_line = -1
            QtWidgets.QToolTip.hideText()

    def _get_popup_rect(self):
        cursor_rec = self.editor.cursorRect()
        char_width = self.editor.fontMetrics().width('A')
        prefix_len = (len(self.completion_prefix) * char_width)
        cursor_rec.translate(
            self.editor.panels.margin_size() - prefix_len,
            self.editor.panels.margin_size(0) + 5)
        popup = self._completer.popup()
        width = popup.verticalScrollBar().sizeHint().width()
        cursor_rec.setWidth(
            self._completer.popup().sizeHintForColumn(0) + width)
        return cursor_rec

    def _show_popup(self, index=0):
        """
        Shows the popup at the specified index.
        :param index: index
        :return:
        """
        full_prefix = self._helper.word_under_cursor(
            select_whole_word=False).selectedText()
        if self._case_sensitive:
            self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive)
        else:
            self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
        # set prefix
        self._completer.setCompletionPrefix(self.completion_prefix)
        cnt = self._completer.completionCount()
        selected = self._completer.currentCompletion()
        if (full_prefix == selected) and cnt == 1:
            debug('user already typed the only completion that we '
                            'have')
            self._hide_popup()
        else:
            # show the completion list
            if self.editor.isVisible():
                if self._completer.widget() != self.editor:
                    self._completer.setWidget(self.editor)
                self._completer.complete(self._get_popup_rect())
                self._completer.popup().setCurrentIndex(
                    self._completer.completionModel().index(index, 0))
                debug(
                    "popup shown: %r" % self._completer.popup().isVisible())
            else:
                debug('cannot show popup, editor is not visible')

    def _show_completions(self, completions):
        debug("showing %d completions" % len(completions))
        debug('popup state: %r', self._completer.popup().isVisible())
        t = time.time()
        self._update_model(completions)
        elapsed = time.time() - t
        debug("completion model updated: %d items in %f seconds",
                        self._completer.model().rowCount(), elapsed)
        self._show_popup()

    def _update_model(self, completions):
        """
        Creates a QStandardModel that holds the suggestion from the completion
        models for the QCompleter

        :param completionPrefix:
        """
        # build the completion model
        cc_model = QtGui.QStandardItemModel()
        self._tooltips.clear()
        for completion in completions:
            name = completion['name']
            item = QtGui.QStandardItem()
            item.setData(name, QtCore.Qt.DisplayRole)
            if 'tooltip' in completion and completion['tooltip']:
                self._tooltips[name] = completion['tooltip']
            if 'icon' in completion:
                icon = completion['icon']
                if isinstance(icon, list):
                    icon = QtGui.QIcon.fromTheme(icon[0], QtGui.QIcon(icon[1]))
                else:
                    icon = QtGui.QIcon(icon)
                item.setData(QtGui.QIcon(icon),
                             QtCore.Qt.DecorationRole)
            cc_model.appendRow(item)
        try:
            self._completer.setModel(cc_model)
        except RuntimeError:
            self._create_completer()
            self._completer.setModel(cc_model)
        return cc_model

    def _display_completion_tooltip(self, completion):
        if not self._show_tooltips:
            return
        if completion not in self._tooltips:
            QtWidgets.QToolTip.hideText()
            return
        tooltip = self._tooltips[completion].strip()
        pos = self._completer.popup().pos()
        pos.setX(pos.x() + self._completer.popup().size().width())
        pos.setY(pos.y() - 15)
        QtWidgets.QToolTip.showText(pos, tooltip, self.editor)

    @staticmethod
    def _is_navigation_key(event):
        return (event.key() == QtCore.Qt.Key_Backspace or
                event.key() == QtCore.Qt.Key_Back or
                event.key() == QtCore.Qt.Key_Delete or
                event.key() == QtCore.Qt.Key_End or
                event.key() == QtCore.Qt.Key_Home or
                event.key() == QtCore.Qt.Key_Left or
                event.key() == QtCore.Qt.Key_Right or
                event.key() == QtCore.Qt.Key_Up or
                event.key() == QtCore.Qt.Key_Down or
                event.key() == QtCore.Qt.Key_Space)