Пример #1
0
 def __init__(self):
     Panel.__init__(self, dynamic=True)
     self.job_runner = DelayJobRunner(delay=500)
     Ui_SearchPanel.__init__(self)
     self.setupUi(self)
     self.toolButtonClose.clicked.connect(self.on_close)
     self.actionSearch.triggered.connect(self.on_search)
     self.actionActionSearchAndReplace.triggered.connect(
         self.on_search_and_replace)
     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)
Пример #2
0
 def __init__(self):
     Panel.__init__(self)
     self._markers = []
     self._icons = {}
     self._previous_line = -1
     self.scrollable = True
     self._job_runner = DelayJobRunner(delay=100)
     self.setMouseTracking(True)
     self._to_remove = []
 def __init__(self):
     Panel.__init__(self, dynamic=True)
     self.job_runner = DelayJobRunner(delay=500)
     Ui_SearchPanel.__init__(self)
     self.setupUi(self)
     self.toolButtonClose.clicked.connect(self.on_close)
     self.actionSearch.triggered.connect(self.on_search)
     self.actionActionSearchAndReplace.triggered.connect(self.on_search_and_replace)
     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)
Пример #4
0
 def __init__(self, worker, delay=500, show_tooltip=True):
     """
     :param worker: The process function or class to call remotely.
     :param delay: The delay used before running the analysis process when
                   trigger is set to
                   :class:pyqode.core.modes.CheckerTriggers`
     :param show_tooltip: Specify if a tooltip must be displayed when the
                          mouse is over a checker message decoration.
     """
     Mode.__init__(self)
     QtCore.QObject.__init__(self)
     # max number of messages to keep good performances
     self.limit = 200
     self._job_runner = DelayJobRunner(delay=delay)
     self._messages = []
     self._worker = worker
     self._mutex = QtCore.QMutex()
     self._show_tooltip = show_tooltip
     self._pending_msg = []
     self._finished = True
Пример #5
0
 def __init__(self):
     Panel.__init__(self)
     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 = -1
     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)
Пример #6
0
 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()
Пример #7
0
 def __init__(self, worker,
              delay=500,
              show_tooltip=True):
     """
     :param worker: The process function or class to call remotely.
     :param delay: The delay used before running the analysis process when
                   trigger is set to
                   :class:pyqode.core.modes.CheckerTriggers`
     :param show_tooltip: Specify if a tooltip must be displayed when the
                          mouse is over a checker message decoration.
     """
     Mode.__init__(self)
     QtCore.QObject.__init__(self)
     # max number of messages to keep good performances
     self.limit = 200
     self._job_runner = DelayJobRunner(delay=delay)
     self._messages = []
     self._worker = worker
     self._mutex = QtCore.QMutex()
     self._show_tooltip = show_tooltip
     self._pending_msg = []
     self._finished = True
 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__(self):
     Panel.__init__(self)
     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 = -1
     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)
Пример #10
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
Пример #11
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))
        except NotRunning:
            QtCore.QTimer.singleShot(100, self.request_search)

    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.set_foreground(QtCore.Qt.black)
        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
Пример #12
0
class CheckerMode(Mode, QtCore.QObject):
    """
    Performs a user defined code analysis job using the backend and
    display the results on the editor instance.

    The user defined code analysis job is a simple **function** with the
    following signature:

    .. code-block:: python

        def analysisProcess(data)

    where data is the request data:

    .. code-block:: python

        request_data = {
                'code': self.editor.toPlainText(),
                'path': self.editor.file.path,
                'encoding': self.editor.file.encoding
            }

    and the return value is a tuple made up of the following elements:

        (description, status, line, [col], [icon], [color], [path])

    The background process is ran when the text changed and the ide is an idle
    state for a few seconds.

    You can also request an analysis manually using
    :meth:`pyqode.core.modes.CheckerMode.request_analysis`

    Messages are displayed as text decorations on the editor. A checker panel
    will take care of display message icons next to each line.
    """
    @property
    def messages(self):
        """
        Returns the entire list of checker messages.
        """
        return self._messages

    def __init__(self, worker,
                 delay=500,
                 show_tooltip=True):
        """
        :param worker: The process function or class to call remotely.
        :param delay: The delay used before running the analysis process when
                      trigger is set to
                      :class:pyqode.core.modes.CheckerTriggers`
        :param show_tooltip: Specify if a tooltip must be displayed when the
                             mouse is over a checker message decoration.
        """
        Mode.__init__(self)
        QtCore.QObject.__init__(self)
        # max number of messages to keep good performances
        self.limit = 200
        self._job_runner = DelayJobRunner(delay=delay)
        self._messages = []
        self._worker = worker
        self._mutex = QtCore.QMutex()
        self._show_tooltip = show_tooltip
        self._pending_msg = []
        self._finished = True

    def add_messages(self, messages):
        """
        Adds a message or a list of message.

        :param messages: A list of messages or a single message
        """
        # remove old messages
        if len(messages) > self.limit:
            messages = messages[:self.limit]
        _logger(self.__class__).debug('adding %s messages' % len(messages))
        self._finished = False
        self._new_messages = messages
        self._to_check = list(self._messages)
        self._pending_msg = messages
        # start removing messages, new message won't be added until we
        # checked all message that need to be removed
        QtCore.QTimer.singleShot(1, self._remove_batch)

    def _remove_batch(self):
        if self.editor is None:
            return
        for i in range(100):
            if not len(self._to_check):
                # all messages checker, start adding messages now
                QtCore.QTimer.singleShot(1, self._add_batch)
                self.editor.repaint()
                return False
            msg = self._to_check.pop(0)
            if msg.block is None:
                msg.block = self.editor.document().findBlockByNumber(msg.line)
            if msg not in self._new_messages:
                self.remove_message(msg)
        self.editor.repaint()
        QtCore.QTimer.singleShot(1, self._remove_batch)

    def _add_batch(self):
        if self.editor is None:
            return
        for i in range(10):
            if not len(self._pending_msg):
                # all pending message added
                self._finished = True
                _logger(self.__class__).debug('finished')
                self.editor.repaint()
                return False
            message = self._pending_msg.pop(0)
            if message.line:
                try:
                    usd = message.block.userData()
                except AttributeError:
                    message.block = self.editor.document().findBlockByNumber(
                        message.line)
                    usd = message.block.userData()
                if usd is None:
                    usd = TextBlockUserData()
                    message.block.setUserData(usd)
                # check if the same message already exists
                if message in usd.messages:
                    continue
                self._messages.append(message)
                usd.messages.append(message)
                tooltip = None
                if self._show_tooltip:
                    tooltip = message.description
                message.decoration = TextDecoration(
                    self.editor.textCursor(), start_line=message.line,
                    tooltip=tooltip, draw_order=3)
                message.decoration.select_line()
                message.decoration.set_as_error(color=QtGui.QColor(
                    message.color))
                self.editor.decorations.append(message.decoration)
        QtCore.QTimer.singleShot(1, self._add_batch)
        self.editor.repaint()
        return True

    def remove_message(self, message):
        """
        Removes a message.

        :param message: Message to remove
        """
        import time
        _logger(self.__class__).debug('removing message %s' % message)
        t = time.time()
        usd = message.block.userData()
        if usd:
            try:
                usd.messages.remove(message)
            except (AttributeError, ValueError):
                pass
        if message.decoration:
            self.editor.decorations.remove(message.decoration)
        self._messages.remove(message)

    def clear_messages(self):
        """
        Clears all messages.
        """
        while len(self._messages):
            msg = self._messages.pop(0)
            usd = msg.block.userData()
            if usd and hasattr(usd, 'messages'):
                usd.messages[:] = []
            if msg.decoration:
                self.editor.decorations.remove(msg.decoration)

    def on_state_changed(self, state):
        if state:
            self.editor.textChanged.connect(self.request_analysis)
            self.editor.new_text_set.connect(self.clear_messages)
            self.request_analysis()
        else:
            self.editor.textChanged.disconnect(self.request_analysis)
            self.editor.new_text_set.disconnect(self.clear_messages)
            self._job_runner.cancel_requests()
            self.clear_messages()

    def _on_work_finished(self, status, results):
        """
        Display results.

        :param status: Response status
        :param results: Response data, messages.
        """
        if status:
            messages = []
            for msg in results:
                msg = CheckerMessage(*msg)
                block = self.editor.document().findBlockByNumber(msg.line)
                msg.block = block
                messages.append(msg)
            self.add_messages(messages)

    def request_analysis(self):
        """
        Requests an analysis.
        """
        if self._finished:
            _logger(self.__class__).debug('running analysis')
            self._job_runner.request_job(self._request)
        elif self.editor:
            # retry later
            _logger(self.__class__).debug(
                'delaying analysis (previous analysis not finished)')
            QtCore.QTimer.singleShot(500, self.request_analysis)

    def _request(self):
        """ Requests a checking of the editor content. """
        if not self.editor:
            return
        try:
            request_data = {
                'code': self.editor.toPlainText(),
                'path': self.editor.file.path,
                'encoding': self.editor.file.encoding
            }
        except RuntimeError:
            return
        try:
            self.editor.backend.send_request(
                self._worker, request_data, on_receive=self._on_work_finished)
            self._finished = False
        except NotRunning:
            # retry later
            QtCore.QTimer.singleShot(100, self._request)
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
Пример #14
0
class CodeEdit(QtWidgets.QPlainTextEdit):
    """
    The editor widget is a simple extension to QPlainTextEdit.

    It adds a few utility signals/methods and introduces the concepts of
    **Managers, Modes and Panels**.

    A **mode/panel** is an editor extension that, once added to a CodeEdit
    instance, may modify its behaviour and appearance:

      * **Modes** are simple objects which connect to the editor signals to
        append new behaviours (such as automatic indentation, code completion,
        syntax checking,...)

      * **Panels** are the combination of a **Mode** and a **QWidget**.
        They are displayed in the CodeEdit's content margins.

        When you install a Panel on a CodeEdit, you can choose to install it in
        one of the four following zones:

            .. image:: _static/editor_widget.png
                :align: center
                :width: 600
                :height: 450

    A **manager** is an object that literally manage a specific aspect of
    :class:`pyqode.core.api.CodeEdit`. There are managers to manage the list of
    modes/panels, to open/save file and to control the backend:

        - :attr:`pyqode.core.api.CodeEdit.file`:
            File manager. Use it to open/save files or access the opened file
            attribute.
        - :attr:`pyqode.core.api.CodeEdit.backend`:
            Backend manager. Use it to start/stop the backend or send a work
            request.
        - :attr:`pyqode.core.api.CodeEdit.modes`:
            Modes manager. Use it to append/remove modes on the editor.
        - :attr:`pyqode.core.api.CodeEdit.panels`:
            Modes manager. Use it to append/remove panels on the editor.

    Starting from version 2.1, CodeEdit defines the
    :attr:`pyqode.core.api.CodeEdit.mimetypes` class attribute that can be used
    by IDE to determine which editor to use for a given mime type. This
    property is a list of supported mimetypes. An empty list means the
    CodeEdit is generic. **Code editors specialised for a specific language
    should define the mime types they support!**
    """
    #: Paint hook
    painted = QtCore.Signal(QtGui.QPaintEvent)
    #: Signal emitted when a new text is set on the widget
    new_text_set = QtCore.Signal()
    #: Signal emitted when the text is saved to file
    text_saved = QtCore.Signal(str)
    #: Signal emitted before the text is saved to file
    text_saving = QtCore.Signal(str)
    #: Signal emitted when the dirty state changed
    dirty_changed = QtCore.Signal(bool)
    #: Signal emitted when a key is pressed
    key_pressed = QtCore.Signal(QtGui.QKeyEvent)
    #: Signal emitted when a key is released
    key_released = QtCore.Signal(QtGui.QKeyEvent)
    #: Signal emitted when a mouse button is pressed
    mouse_pressed = QtCore.Signal(QtGui.QMouseEvent)
    #: Signal emitted when a mouse button is released
    mouse_released = QtCore.Signal(QtGui.QMouseEvent)
    #: Signal emitted when a mouse double click event occured
    mouse_double_clicked = QtCore.Signal(QtGui.QMouseEvent)
    #: Signal emitted on a wheel event
    mouse_wheel_activated = QtCore.Signal(QtGui.QWheelEvent)
    #: Signal emitted at the end of the key_pressed event
    post_key_pressed = QtCore.Signal(QtGui.QKeyEvent)
    #: Signal emitted when focusInEvent is is called
    focused_in = QtCore.Signal(QtGui.QFocusEvent)
    #: Signal emitted when the mouse_moved
    mouse_moved = QtCore.Signal(QtGui.QMouseEvent)
    #: Signal emitted when the user press the TAB key
    indent_requested = QtCore.Signal()
    #: Signal emitted when the user press the BACK-TAB (Shift+TAB) key
    unindent_requested = QtCore.Signal()

    #: Store the list of mimetypes associated with the editor, for
    #: specialised editors.
    mimetypes = []

    _DEFAULT_FONT = 'Source Code Pro' if sys.platform != 'darwin' else 'Monaco'

    @property
    def use_spaces_instead_of_tabs(self):
        """ Use spaces instead of tabulations. Default is True. """
        return self._use_spaces_instead_of_tabs

    @use_spaces_instead_of_tabs.setter
    def use_spaces_instead_of_tabs(self, value):
        self._use_spaces_instead_of_tabs = value
        for c in self.clones:
            c.use_spaces_instead_of_tabs = value

    @property
    def tab_length(self):
        """ Tab length, number of spaces. """
        return self._tab_length

    @tab_length.setter
    def tab_length(self, value):
        self._tab_length = value
        for c in self.clones:
            c.tab_length = value

    @property
    def save_on_focus_out(self):
        """
        Automatically saves editor content on focus out.

        Default is False.
        """
        return self._save_on_focus_out

    @save_on_focus_out.setter
    def save_on_focus_out(self, value):
        self._save_on_focus_out = value
        for c in self.clones:
            c.save_on_focus_out = value

    @property
    def show_whitespaces(self):
        """
        Shows/Hides virtual white spaces.
        """
        return self._show_whitespaces

    @show_whitespaces.setter
    def show_whitespaces(self, value):
        self._show_whitespaces = value
        self._set_whitespaces_flags(value)
        for c in self.clones:
            c.show_whitespaces = value

    @property
    def font_name(self):
        """
        The editor font family name.
        """
        return self._font_family

    @font_name.setter
    def font_name(self, value):
        if value == "":
            value = self._DEFAULT_FONT
        self._font_family = value
        self._reset_stylesheet()
        for c in self.clones:
            c.font_name = value

    @property
    def zoom_level(self):
        """
        Gets/Sets the editor zoom level.

        The zoom level is a value that is added to the current editor font
        size. Negative values are used to zoom out the editor, positive values
        are used to zoom in the editor.
        """
        return self._zoom_level

    @zoom_level.setter
    def zoom_level(self, value):
        self._zoom_level = value

    @property
    def font_size(self):
        """
        The font point size.

        .. note:: Please, **never use setFontPointSize/setFontFamily functions
            directly** as the values you define there will be overwritten as
            soon as the user zoom the editor or as soon as a stylesheet
            property has changed.
        """
        return self._font_size

    @font_size.setter
    def font_size(self, value):
        self._font_size = value
        self._reset_stylesheet()
        for c in self.clones:
            c.font_size = value

    @property
    def background(self):
        """
        The editor background color (QColor)
        """
        return self._background

    @background.setter
    def background(self, value):
        self._background = value
        self._reset_stylesheet()
        for c in self.clones:
            c.background = value

    @property
    def foreground(self):
        """
        The editor foreground color (QColor)
        """
        return self._foreground

    @foreground.setter
    def foreground(self, value):
        self._foreground = value
        self._reset_stylesheet()
        for c in self.clones:
            c.foreground = value

    @property
    def whitespaces_foreground(self):
        """
        The editor white spaces' foreground color. White spaces are highlighted
        by the syntax highlighter. You should call rehighlight to update their
        color. This is not done automatically to prevent multiple, useless
        call to ``rehighlight`` which can take some time on big files.
        """
        return self._whitespaces_foreground

    @whitespaces_foreground.setter
    def whitespaces_foreground(self, value):
        self._whitespaces_foreground = value
        for c in self.clones:
            c.whitespaces_foreground = value

    @property
    def selection_background(self):
        """
        The editor selection's background color.
        """
        return self._sel_background

    @selection_background.setter
    def selection_background(self, value):
        self._sel_background = value
        self._reset_stylesheet()
        for c in self.clones:
            c.selection_background = value

    @property
    def selection_foreground(self):
        """
        The editor selection's foreground color.
        """
        return self._sel_foreground

    @selection_foreground.setter
    def selection_foreground(self, value):
        self._sel_foreground = value
        for c in self.clones:
            c.selection_foreground = value

    @property
    def word_separators(self):
        """
        The list of word separators used by the code completion mode
        and the word clicked mode.
        """
        return self._word_separators

    @word_separators.setter
    def word_separators(self, value):
        self._word_separators = value
        for c in self.clones:
            c._word_separators = value

    @property
    def dirty(self):
        """
        Tells whethere the content of editor has been modified.

        (this is just a shortcut to QTextDocument.isModified

        :type: bool
        """
        return self.document().isModified()

    @property
    def visible_blocks(self):
        """
        Returns the list of visible blocks.

        Each element in the list is a tuple made up of the line top position,
        the line number and the QTextBlock itself.

        :return: A list of tuple(top_position, line_number, block)
        :rtype: List of tuple(int, int, QtWidgets.QTextBlock)
        """
        return self._visible_blocks

    @property
    def file(self):
        """
        Returns a reference to the :class:`pyqode.core.managers.FileManager`
        used to open/save file on the editor
        """
        return self._file

    @file.setter
    def file(self, file_manager):
        """
        Sets a custom file manager.

        :param file_manager: custom file manager instance.
        """
        self._file = file_manager

    @property
    def backend(self):
        """
        Returns a reference to the :class:`pyqode.core.managers.BackendManager`
        used to control the backend process.
        """
        return self._backend

    @property
    def modes(self):
        """
        Returns a reference to the :class:`pyqode.core.managers.ModesManager`
        used to manage the collection of installed modes.
        """
        return self._modes

    @property
    def panels(self):
        """
        Returns a reference to the :class:`pyqode.core.managers.PanelsManager`
        used to manage the collection of installed panels
        """
        return self._panels

    @property
    def decorations(self):
        """
        Returns a reference to the
        :class:`pyqode.core.managers.TextDecorationManager` used to manage the
        list of :class:`pyqode.core.api.TextDecoration`
        """
        return self._decorations

    @property
    def syntax_highlighter(self):
        """
        Returns a reference to the syntax highlighter mode currently used to
        highlight the editor content.

        :return: :class:`pyqode.core.api.SyntaxHighlighter`
        """
        for mode in self.modes:
            if hasattr(mode, 'highlightBlock'):
                return mode
        return None

    def __init__(self, parent=None, create_default_actions=True):
        """
        :param parent: Parent widget

        :param create_default_actions: True to create the default context
            menu actions (copy, paste, edit)
        """
        super(CodeEdit, self).__init__(parent)
        self.clones = []
        self._default_font_size = 10
        self._backend = BackendManager(self)
        self._file = FileManager(self)
        self._modes = ModesManager(self)
        self._panels = PanelsManager(self)
        self._decorations = TextDecorationsManager(self)
        self.document().modificationChanged.connect(self._emit_dirty_changed)

        self._word_separators = [
            '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '{',
            '}', '|', ':', '"', "'", "<", ">", "?", ",", ".", "/", ";", '[',
            ']', '\\', '\n', '\t', '=', '-', ' '
        ]
        self._save_on_focus_out = False
        self._use_spaces_instead_of_tabs = True
        self._whitespaces_foreground = None
        self._sel_background = None
        self._show_whitespaces = False
        self._foreground = None
        self._sel_foreground = None
        self._tab_length = 4
        self._zoom_level = 0
        self._font_size = 10
        self._background = None
        QtGui.QFontDatabase.addApplicationFont(
            ':/fonts/rc/SourceCodePro-Regular.ttf')
        QtGui.QFontDatabase.addApplicationFont(
            ':/fonts/rc/SourceCodePro-Bold.ttf')
        self._font_family = self._DEFAULT_FONT
        self._mimetypes = []

        # Flags/Working variables
        self._last_mouse_pos = QtCore.QPoint(0, 0)
        self._modified_lines = set()
        self._cleaning = False
        self._visible_blocks = []
        self._tooltips_runner = DelayJobRunner(delay=700)
        self._prev_tooltip_block_nbr = -1
        self._original_text = ""

        self._dirty = False

        # setup context menu
        self._actions = []
        self._menus = []
        if create_default_actions:
            self._init_actions()
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._show_context_menu)
        self._mnu = None  # bug with PySide (github #63)

        # init settings and styles from global settings/style modules
        self._init_settings()
        self._init_style()

        # connect slots
        self.textChanged.connect(self._on_text_changed)
        self.blockCountChanged.connect(self.update)
        self.cursorPositionChanged.connect(self.update)
        self.selectionChanged.connect(self.update)

        self.setMouseTracking(True)
        self.setCenterOnScroll(True)
        self.setLineWrapMode(self.NoWrap)

    def __repr__(self):
        return '%s(path=%r)' % (self.__class__.__name__, self.file.path)

    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

    def clone(self):
        """
        Clone ourselves, return an instance of the same class, using the
        default QWidget constructor.
        """
        clone = self.__class__(parent=self.parent())
        return clone

    def link(self, clone):
        """
        Links the clone with its original. We copy the file manager infos
        (path, mimetype, ...) and setup the clone text document as reference
        to our text document.

        :param clone: clone to link.
        """
        clone.file._path = self.file.path
        clone.file._encoding = self.file.encoding
        clone.file._mimetype = self.file.mimetype
        clone.setDocument(self.document())
        for original_mode, mode in zip(list(self.modes), list(clone.modes)):
            mode.enabled = original_mode.enabled
            mode.clone_settings(original_mode)
        for original_panel, panel in zip(
                list(self.panels), list(clone.panels)):
            panel.enabled = original_panel.isEnabled()
            panel.clone_settings(original_panel)
            if not original_panel.isVisible():
                panel.setVisible(False)
        clone.use_spaces_instead_of_tabs = self.use_spaces_instead_of_tabs
        clone.tab_length = self.tab_length
        clone.save_on_focus_out = self.save_on_focus_out
        clone.show_whitespaces = self.show_whitespaces
        clone.font_name = self.font_name
        clone.font_size = self.font_size
        clone.zoom_level = self.zoom_level
        clone.background = self.background
        clone.foreground = self.foreground
        clone.whitespaces_foreground = self.whitespaces_foreground
        clone.selection_background = self.selection_background
        clone.selection_foreground = self.selection_foreground
        clone.word_separators = self.word_separators
        clone.file.clone_settings(self.file)

    def close(self, clear=True):
        """
        Closes the editor, stops the backend and removes any installed
        mode/panel.

        This is also where we cache the cursor position.

        :param clear: True to clear the editor content before closing.
        """
        self.decorations.clear()
        self.modes.clear()
        self.panels.clear()
        self.backend.stop()
        Cache().set_cursor_position(
            self.file.path, TextHelper(self).cursor_position())
        super(CodeEdit, self).close()
        _logger().info('closed')

    def set_mouse_cursor(self, cursor):
        """
        Changes the viewport's cursor

        :param cursor: the mouse cursor to set.
        :type cursor: QtWidgets.QCursor
        """
        self.viewport().setCursor(cursor)

    def show_tooltip(self, pos, tooltip, _sender_deco=None):
        """
        Show a tool tip at the specified position

        :param pos: Tooltip position
        :param tooltip: Tooltip text

        :param _sender_deco: TextDecoration which is the sender of the show
            tooltip request. (for internal use only).
        """
        if _sender_deco is not None and _sender_deco not in self.decorations:
            return
        QtWidgets.QToolTip.showText(pos, tooltip[0: 1024], self)

    def setPlainText(self, txt, mime_type, encoding):
        """
        Extends setPlainText to force the user to setup an encoding and a
        mime type.

        Emits the new_text_set signal.

        :param txt: The new text to set.
        :param mime_type: Associated mimetype. Setting the mime will update the
                          pygments lexer.
        :param encoding: text encoding
        """
        self.file.mimetype = mime_type
        self.file._encoding = encoding
        self._original_text = txt
        self._modified_lines.clear()
        import time
        t = time.time()
        super(CodeEdit, self).setPlainText(txt)
        _logger().info('setPlainText duration: %fs' % (time.time() - t))
        self.new_text_set.emit()
        self.redoAvailable.emit(False)
        self.undoAvailable.emit(False)

    def add_action(self, action):
        """
        Adds an action to the editor's context menu.

        :param action: QtWidgets.QAction
        """
        self._actions.append(action)
        action.setShortcutContext(QtCore.Qt.WidgetShortcut)
        super(CodeEdit, self).addAction(action)

    def insert_action(self, action, prev_action):
        """
        Inserts an action to the editor's context menu

        :param action: action to insert
        :param prev_action: the action after which the new action must be
            inserted
        """
        index = self._actions.index(prev_action)
        action.setShortcutContext(QtCore.Qt.WidgetShortcut)
        self._actions.insert(index, action)

    def actions(self):
        """
        Returns the list of actions/sepqrators of the context menu.

        """
        return self._actions

    def add_separator(self):
        """
        Adds a sepqrator to the editor's context menu.

        :return: The sepator that has been added.
        :rtype: QtWidgets.QAction
        """
        action = QtWidgets.QAction(self)
        action.setSeparator(True)
        self._actions.append(action)
        self.addAction(action)
        return action

    def remove_action(self, action):
        """
        Removes an action/separator from the editor's context menu.

        :param action: Action/seprator to remove.
        """
        try:
            self._actions.remove(action)
        except ValueError:
            pass
        self.removeAction(action)

    def add_menu(self, menu):
        """
        Adds a sub-menu to the editor context menu.

        Menu are put at the bottom of the context menu.

        .. note:: to add a menu in the middle of the context menu, you can
            always add its menuAction().

        :param menu: menu to add
        """
        self._menus.append(menu)
        for action in menu.actions():
            action.setShortcutContext(QtCore.Qt.WidgetShortcut)
        self.addActions(menu.actions())

    def remove_menu(self, menu):
        """
        Removes a sub-menu from the context menu.
        :param menu: Sub-menu to remove.
        """
        self._menus.remove(menu)
        for action in menu.actions():
            self.removeAction(action)

    @QtCore.Slot()
    def delete(self):
        """ Deletes the selected text """
        self.textCursor().removeSelectedText()

    @QtCore.Slot()
    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)

    @QtCore.Slot()
    def rehighlight(self):
        """
        Calls ``rehighlight`` on the installed syntax highlighter mode.
        """
        if self.syntax_highlighter:
            self.syntax_highlighter.rehighlight()

    @QtCore.Slot()
    def reset_zoom(self):
        """
        Resets the zoom level.
        """
        self._zoom_level = 0
        self._reset_stylesheet()

    @QtCore.Slot()
    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()

    @QtCore.Slot()
    def zoom_out(self, decrement=1):
        """
        Zooms out the editor (makes the font smaller).

        :param decrement: zoom level decrement. Default is 1. The value is
            given as an absolute value.
        """
        self.zoom_level -= decrement
        # make sure font size remains > 0
        if self.font_size + self.zoom_level <= 0:
            self.zoom_level = -self._font_size + 1
        TextHelper(self).mark_whole_doc_dirty()
        self._reset_stylesheet()

    @QtCore.Slot()
    def duplicate_line(self):
        """
        Duplicates the line under the cursor. If multiple lines are selected,
        only the last one is duplicated.
        """
        cursor = self.textCursor()
        assert isinstance(cursor, QtGui.QTextCursor)
        has_selection = True
        if not cursor.hasSelection():
            cursor.select(cursor.LineUnderCursor)
            has_selection = False
        line = cursor.selectedText()
        line = '\n'.join(line.split('\u2029'))
        end = cursor.selectionEnd()
        cursor.setPosition(end)
        cursor.beginEditBlock()
        cursor.insertText('\n')
        cursor.insertText(line)
        cursor.endEditBlock()
        if has_selection:
            pos = cursor.position()
            cursor.setPosition(end+1)
            cursor.setPosition(pos, cursor.KeepAnchor)
        self.setTextCursor(cursor)


    @QtCore.Slot()
    def indent(self):
        """
        Indents the text cursor or the selection.

        Emits the :attr:`pyqode.core.api.CodeEdit.indent_requested`
        signal, the :class:`pyqode.core.modes.IndenterMode` will
        perform the actual indentation.
        """
        self.indent_requested.emit()

    @QtCore.Slot()
    def un_indent(self):
        """
        Un-indents the text cursor or the selection.

        Emits the :attr:`pyqode.core.api.CodeEdit.unindent_requested`
        signal, the :class:`pyqode.core.modes.IndenterMode` will
        perform the actual un-indentation.
        """
        self.unindent_requested.emit()

    def resizeEvent(self, e):
        """
        Overrides resize event to resize the editor's panels.

        :param e: resize event
        """
        super(CodeEdit, self).resizeEvent(e)
        self.panels.resize()

    def paintEvent(self, e):
        """
        Overrides paint event to update the list of visible blocks and emit
        the painted event.

        :param e: paint event
        """
        self._update_visible_blocks(e)
        super(CodeEdit, self).paintEvent(e)
        self.painted.emit(e)

    def keyPressEvent(self, event):
        """
        Overrides the keyPressEvent to emit the key_pressed signal.

        Also takes care of indenting and handling smarter home key.

        :param event: QKeyEvent
        """
        initial_state = event.isAccepted()
        event.ignore()
        self.key_pressed.emit(event)
        state = event.isAccepted()
        if not event.isAccepted():
            if event.key() == QtCore.Qt.Key_Tab:
                self.indent()
                event.accept()
            elif event.key() == QtCore.Qt.Key_Backtab:
                self.un_indent()
                event.accept()
            elif event.key() == QtCore.Qt.Key_Home:
                self._do_home_key(
                    event, int(event.modifiers()) & QtCore.Qt.ShiftModifier)
            if not event.isAccepted():
                event.setAccepted(initial_state)
                super(CodeEdit, self).keyPressEvent(event)
        new_state = event.isAccepted()
        event.setAccepted(state)
        self.post_key_pressed.emit(event)
        event.setAccepted(new_state)

    def keyReleaseEvent(self, event):
        """
        Overrides keyReleaseEvent to emit the key_released signal.

        :param event: QKeyEvent
        """
        initial_state = event.isAccepted()
        event.ignore()
        self.key_released.emit(event)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).keyReleaseEvent(event)

    def mouseDoubleClickEvent(self, event):
        initial_state = event.isAccepted()
        event.ignore()
        self.mouse_double_clicked.emit(event)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).mouseDoubleClickEvent(event)

    def focusInEvent(self, event):
        """
        Overrides focusInEvent to emits the focused_in signal

        :param event: QFocusEvent
        """
        # fix a visual bug if the editor was resized while being hidden (
        # e.g. a dock # widget has been resized and some editors were in a
        # tab widget. Non visible editor have a visual bug where horizontal
        # scroll bar range
        #
        TextHelper(self).mark_whole_doc_dirty()
        s = self.size()
        s.setWidth(s.width() + 1)
        self.resizeEvent(QtGui.QResizeEvent(self.size(), s))

        self.focused_in.emit(event)
        super(CodeEdit, self).focusInEvent(event)
        self.repaint()

    def focusOutEvent(self, event):
        # Saves content if save_on_focus_out is True.
        if self.save_on_focus_out and self.dirty and self.file.path:
            self.file.save()
        super(CodeEdit, self).focusOutEvent(event)

    def mousePressEvent(self, event):
        """
        Overrides mousePressEvent to emits mouse_pressed signal

        :param event: QMouseEvent
        """
        initial_state = event.isAccepted()
        event.ignore()
        self.mouse_pressed.emit(event)
        if event.button() == QtCore.Qt.LeftButton:
            cursor = self.cursorForPosition(event.pos())
            for sel in self.decorations:
                if sel.cursor.blockNumber() == cursor.blockNumber():
                    if sel.contains_cursor(cursor):
                        sel.signals.clicked.emit(sel)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        """
        Emits mouse_released signal.

        :param event: QMouseEvent
        """
        initial_state = event.isAccepted()
        event.ignore()
        self.mouse_released.emit(event)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).mouseReleaseEvent(event)

    def wheelEvent(self, event):
        """
        Emits the mouse_wheel_activated signal.

        :param event: QMouseEvent
        """
        initial_state = event.isAccepted()
        event.ignore()
        self.mouse_wheel_activated.emit(event)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).wheelEvent(event)

    def mouseMoveEvent(self, event):
        """
        Overrides mouseMovedEvent to display any decoration tooltip and emits
        the mouse_moved event.

        :param event: QMouseEvent
        """
        cursor = self.cursorForPosition(event.pos())
        self._last_mouse_pos = event.pos()
        block_found = False
        for sel in self.decorations:
            if sel.contains_cursor(cursor) and sel.tooltip:
                if (self._prev_tooltip_block_nbr != cursor.blockNumber() or
                        not QtWidgets.QToolTip.isVisible()):
                    pos = event.pos()
                    # add left margin
                    pos.setX(pos.x() + self.panels.margin_size())
                    # add top margin
                    pos.setY(pos.y() + self.panels.margin_size(0))
                    self._tooltips_runner.request_job(
                        self.show_tooltip,
                        self.mapToGlobal(pos), sel.tooltip[0: 1024], sel)
                    self._prev_tooltip_block_nbr = cursor.blockNumber()
                block_found = True
                break
        if not block_found and self._prev_tooltip_block_nbr != -1:
            QtWidgets.QToolTip.hideText()
            self._prev_tooltip_block_nbr = -1
            self._tooltips_runner.cancel_requests()
        self.mouse_moved.emit(event)
        super(CodeEdit, self).mouseMoveEvent(event)

    def showEvent(self, event):
        """ Overrides showEvent to update the viewport margins """
        super(CodeEdit, self).showEvent(event)
        _logger().debug('show event: %r' % self)

    def get_context_menu(self):
        """
        Gets the editor context menu.

        :return: QMenu
        """
        mnu = QtWidgets.QMenu()
        mnu.addActions(self._actions)
        mnu.addSeparator()
        for menu in self._menus:
            mnu.addMenu(menu)
        return mnu

    def _show_context_menu(self, point):
        """ Shows the context menu """
        self._mnu = self.get_context_menu()
        self._mnu.popup(self.mapToGlobal(point))

    def _set_whitespaces_flags(self, show):
        """ Sets show white spaces flag """
        doc = self.document()
        options = doc.defaultTextOption()
        if show:
            options.setFlags(options.flags() |
                             QtGui.QTextOption.ShowTabsAndSpaces)
        else:
            options.setFlags(options.flags() &
                             ~QtGui.QTextOption.ShowTabsAndSpaces)
        doc.setDefaultTextOption(options)

    def _init_actions(self):
        """ Init default QAction """
        def _icon(val):
            if isinstance(val, tuple):
                theme, icon = val
                return QtGui.QIcon.fromTheme(theme, QtGui.QIcon(icon))
            else:
                QtGui.QIcon(val)
        # Undo
        action = QtWidgets.QAction('Undo', self)
        action.setShortcut(QtGui.QKeySequence.Undo)
        action.setIcon(_icon(('edit-undo', ':/pyqode-icons/rc/edit-undo.png')))
        action.triggered.connect(self.undo)
        self.undoAvailable.connect(action.setEnabled)
        self.add_action(action)
        self.action_undo = action
        # Redo
        action = QtWidgets.QAction('Redo', self)
        action.setShortcut(QtGui.QKeySequence.Redo)
        action.setIcon(_icon(('edit-redo', ':/pyqode-icons/rc/edit-redo.png')))
        action.triggered.connect(self.redo)
        self.redoAvailable.connect(action.setEnabled)
        self.add_action(action)
        self.action_redo = action
        # separator
        self.add_separator()
        # Copy
        action = QtWidgets.QAction('Copy', self)
        action.setShortcut(QtGui.QKeySequence.Copy)
        action.setIcon(_icon(('edit-copy', ':/pyqode-icons/rc/edit-copy.png')))
        action.triggered.connect(self.copy)
        self.copyAvailable.connect(action.setEnabled)
        self.add_action(action)
        self.action_copy = action
        # cut
        action = QtWidgets.QAction('Cut', self)
        action.setShortcut(QtGui.QKeySequence.Cut)
        action.setIcon(_icon(('edit-cut', ':/pyqode-icons/rc/edit-cut.png')))
        action.triggered.connect(self.cut)
        self.copyAvailable.connect(action.setEnabled)
        self.add_action(action)
        self.action_cut = action
        # paste
        action = QtWidgets.QAction('Paste', self)
        action.setShortcut(QtGui.QKeySequence.Paste)
        action.setIcon(_icon(('edit-paste',
                              ':/pyqode-icons/rc/edit-paste.png')))
        action.triggered.connect(self.paste)
        self.add_action(action)
        self.action_paste = action
        # delete
        action = QtWidgets.QAction('Delete', self)
        action.setShortcut(QtGui.QKeySequence.Delete)
        action.setIcon(_icon(('edit-delete',
                              ':/pyqode-icons/rc/edit-delete.png')))
        action.triggered.connect(self.delete)
        self.add_action(action)
        self.action_delete = action
        # duplicate line
        action = QtWidgets.QAction('Duplicate line', self)
        action.setShortcut('Ctrl+D')
        action.triggered.connect(self.duplicate_line)
        self.add_action(action)
        self.action_duplicate_line = action
        # select all
        action = QtWidgets.QAction('Select all', self)
        action.setShortcut(QtGui.QKeySequence.SelectAll)
        action.setIcon(_icon(('edit-select-all',
                              ':/pyqode-icons/rc/edit-select-all.png')))
        action.triggered.connect(self.selectAll)
        self.add_action(action)
        self.action_select_all = action
        # separator
        self.add_separator()
        # indent
        action = QtWidgets.QAction('Indent', self)
        action.setShortcut('Tab')
        action.setIcon(_icon(('format-indent-more',
                              ':/pyqode-icons/rc/format-indent-more.png')))
        action.triggered.connect(self.indent)
        self.add_action(action)
        self.action_indent = action
        # unindent
        action = QtWidgets.QAction('Un-indent', self)
        action.setShortcut('Shift+Tab')
        action.setIcon(_icon(('format-indent-less',
                              ':/pyqode-icons/rc/format-indent-less.png')))
        action.triggered.connect(self.un_indent)
        self.add_action(action)
        self.action_un_indent = action
        # separator
        self.add_separator()
        # goto
        action = QtWidgets.QAction('Go to line', self)
        action.setShortcut('Ctrl+G')
        action.setIcon(_icon(('go-jump',
                              ':/pyqode-icons/rc/goto-line.png')))
        action.triggered.connect(self.goto_line)
        self.add_action(action)
        self.action_goto_line = action
        self.add_separator()

    def _init_settings(self):
        """ Init setting """
        self._show_whitespaces = False
        self._tab_length = 4
        self._use_spaces_instead_of_tabs = True
        self.setTabStopWidth(self._tab_length *
                             self.fontMetrics().width(" "))
        self._set_whitespaces_flags(self._show_whitespaces)

    def _init_style(self):
        """ Inits style options """
        self._background = QtGui.QColor('white')
        self._foreground = QtGui.QColor('black')
        self._whitespaces_foreground = QtGui.QColor('light gray')
        app = QtWidgets.QApplication.instance()
        self._sel_background = app.palette().highlight().color()
        self._sel_foreground = app.palette().highlightedText().color()
        self._font_size = 10
        self.font_name = ""

    def _update_visible_blocks(self, *args):
        """ Updates the list of visible blocks """
        self._visible_blocks[:] = []
        block = self.firstVisibleBlock()
        block_nbr = block.blockNumber()
        top = int(self.blockBoundingGeometry(block).translated(
            self.contentOffset()).top())
        bottom = top + int(self.blockBoundingRect(block).height())
        ebottom_top = 0
        ebottom_bottom = self.height()
        while block.isValid():
            visible = (top >= ebottom_top and bottom <= ebottom_bottom)
            if not visible:
                break
            if block.isVisible():
                self._visible_blocks.append((top, block_nbr, block))
            block = block.next()
            top = bottom
            bottom = top + int(self.blockBoundingRect(block).height())
            block_nbr = block.blockNumber()

    def _on_text_changed(self):
        """ Adjust dirty flag depending on editor's content """
        if not self._cleaning:
            ln = TextHelper(self).cursor_position()[0]
            self._modified_lines.add(ln)

    def _reset_stylesheet(self):
        """ Resets stylesheet"""
        self.setFont(QtGui.QFont(self._font_family,
                                 self._font_size + self._zoom_level))
        p = self.palette()
        p.setColor(QtGui.QPalette.Base, self.background)
        p.setColor(QtGui.QPalette.Text, self.foreground)
        p.setColor(QtGui.QPalette.Highlight, self.selection_background)
        p.setColor(QtGui.QPalette.HighlightedText, self.selection_foreground)
        if QtWidgets.QApplication.instance().styleSheet() or (
                hasattr(self, '_flg_stylesheet') and
                platform.system() == 'Windows'):
            # on windows, if the application once had a stylesheet, we must
            # keep on using a stylesheet otherwise strange colors appear
            # see https://github.com/OpenCobolIDE/OpenCobolIDE/issues/65
            self._flg_stylesheet = True
            self.setStyleSheet('''QPlainTextEdit
            {
                background-color: %s;
                color: %s;
            }
            ''' % (self.background.name(), self.foreground.name()))
        else:
            # on linux/osx we just have to set an empty stylesheet to cancel
            # any previous stylesheet and still keep a correct style for
            # scrollbars
            self.setStyleSheet('')
        self.setPalette(p)
        self.repaint()

    def _do_home_key(self, event=None, select=False):
        """ Performs home key action """
        # get nb char to first significative char
        delta = (self.textCursor().positionInBlock() -
                 TextHelper(self).line_indent())
        cursor = self.textCursor()
        move = QtGui.QTextCursor.MoveAnchor
        if select:
            move = QtGui.QTextCursor.KeepAnchor
        if delta > 0:
            cursor.movePosition(QtGui.QTextCursor.Left, move, delta)
        else:
            cursor.movePosition(QtGui.QTextCursor.StartOfBlock, move)
        self.setTextCursor(cursor)
        if event:
            event.accept()

    def _emit_dirty_changed(self, state):
        self.dirty_changed.emit(state)
        for c in self.clones:
            c.dirty_changed.emit(state)
Пример #15
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.

    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
Пример #16
0
class MarkerPanel(Panel):
    """
    General purpose marker panel.
    This panels takes care of drawing icons at a specific line number.

    Use addMarker, removeMarker and clearMarkers to manage the collection of
    displayed makers.

    You can create a user editable panel (e.g. a breakpoints panel) by using
    the following signals:

      - :attr:`pyqode.core.panels.MarkerPanel.add_marker_requested`
      - :attr:`pyqode.core.panels.MarkerPanel.remove_marker_requested`

    """
    #: Signal emitted when the user clicked in a place where there is no
    #: marker.
    add_marker_requested = QtCore.Signal(int)
    #: Signal emitted when the user clicked on an existing marker.
    remove_marker_requested = QtCore.Signal(int)

    def __init__(self):
        Panel.__init__(self)
        self._markers = []
        self._icons = {}
        self._previous_line = -1
        self.scrollable = True
        self._job_runner = DelayJobRunner(delay=100)
        self.setMouseTracking(True)
        self._to_remove = []

    def add_marker(self, marker):
        """
        Adds the marker to the panel.

        :param marker: Marker to add
        :type marker: pyqode.core.modes.Marker
        """
        key, val = self.make_marker_icon(marker.icon)
        if key and val:
            self._icons[key] = val
        self._markers.append(marker)
        doc = self.editor.document()
        assert isinstance(doc, QtGui.QTextDocument)
        block = doc.findBlockByLineNumber(marker.position - 1)
        user_data = block.userData()
        if user_data is None:
            user_data = TextBlockUserData()
            block.setUserData(user_data)
        marker.panel_ref = self
        user_data.markers.append(marker)
        self.repaint()

    @staticmethod
    @memoized
    def make_marker_icon(icon):
        """
        Make (and memoize) an icon from an icon filename.

        :param icon: Icon filename or tuple (to use a theme).
        """
        if isinstance(icon, tuple):
            return icon[0], QtGui.QIcon.fromTheme(
                icon[0], QtGui.QIcon(icon[1]))
        elif isinstance(icon, str):
            return icon, QtGui.QIcon(icon)
        else:
            return None, None

    def remove_marker(self, marker):
        """
        Removes a marker from the panel

        :param marker: Marker to remove
        :type marker: pyqode.core.Marker
        """
        self._markers.remove(marker)
        self._to_remove.append(marker)
        self.repaint()

    def clear_markers(self):
        """ Clears the markers list """
        while len(self._markers):
            self.remove_marker(self._markers[0])

    def marker_for_line(self, line):
        """
        Returns the marker that is displayed at the specified line number if
        any.

        :param line: The marker line.

        :return: Marker of None
        :rtype: pyqode.core.Marker
        """
        markers = []
        for marker in self._markers:
            if line == marker.position:
                markers.append(marker)
        return markers

    def sizeHint(self):
        """
        Returns the panel size hint. (fixed with of 16px)
        """
        metrics = QtGui.QFontMetricsF(self.editor.font())
        size_hint = QtCore.QSize(metrics.height(), metrics.height())
        if size_hint.width() > 16:
            size_hint.setWidth(16)
        return size_hint

    def paintEvent(self, event):
        Panel.paintEvent(self, event)
        painter = QtGui.QPainter(self)
        for top, block_nbr, block in self.editor.visible_blocks:
            user_data = block.userData()
            if hasattr(user_data, "markers"):
                markers = user_data.markers
                for i, marker in enumerate(markers):
                    if (hasattr(marker, 'panel_ref') and
                            marker.panel_ref == self):
                        # only draw our markers
                        if marker in self._to_remove:
                            try:
                                user_data.markers.remove(None)
                                self._to_remove.remove(marker)
                            except ValueError:
                                pass
                            continue
                        if marker and marker.icon:
                            rect = QtCore.QRect()
                            rect.setX(0)
                            rect.setY(top)
                            rect.setWidth(self.sizeHint().width())
                            rect.setHeight(self.sizeHint().height())
                            if isinstance(marker.icon, tuple):
                                key = marker.icon[0]
                            else:
                                key = marker.icon
                            if key not in self._icons:
                                key, val = self.make_marker_icon(marker.icon)
                                if key and val:
                                    self._icons[key] = val
                                else:
                                    continue
                            self._icons[key].paint(painter, rect)

    def mousePressEvent(self, event):
        # Handle mouse press:
        # - emit add marker signal if there were no marker under the mouse
        #   cursor
        # - emit remove marker signal if there were one or more markers under
        #   the mouse cursor.
        line = TextHelper(self.editor).line_nbr_from_position(event.pos().y())
        if self.marker_for_line(line):
            _logger().debug("remove marker requested")
            self.remove_marker_requested.emit(line)
        else:
            _logger().debug("add marker requested")
            self.add_marker_requested.emit(line)

    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

    def leaveEvent(self, *args, **kwargs):
        """
        Hide tooltip when leaving the panel region.
        """
        QtWidgets.QToolTip.hideText()
        self._previous_line = -1

    def _display_tooltip(self, tooltip, top):
        """
        Display tooltip at the specified top position.
        """
        QtWidgets.QToolTip.showText(self.mapToGlobal(QtCore.QPoint(
            self.sizeHint().width(), top)), tooltip, self)
Пример #17
0
class MarkerPanel(Panel):
    """
    General purpose marker panel.
    This panels takes care of drawing icons at a specific line number.

    Use addMarker, removeMarker and clearMarkers to manage the collection of
    displayed makers.

    You can create a user editable panel (e.g. a breakpoints panel) by using
    the following signals:

      - :attr:`pyqode.core.panels.MarkerPanel.add_marker_requested`
      - :attr:`pyqode.core.panels.MarkerPanel.remove_marker_requested`

    """
    #: Signal emitted when the user clicked in a place where there is no
    #: marker.
    add_marker_requested = QtCore.Signal(int)
    #: Signal emitted when the user right clicked on an existing marker.
    edit_marker_requested = QtCore.Signal(int)
    #: Signal emitted when the user left clicked on an existing marker.
    remove_marker_requested = QtCore.Signal(int)

    @property
    def background(self):
        """
        Marker background color in editor. Use None if no text decoration
        should be used.
        """
        return self._background

    @background.setter
    def background(self, value):
        self._background = value

    def __init__(self):
        Panel.__init__(self)
        self._background = QtGui.QColor('#FFC8C8')
        self._markers = []
        self._icons = {}
        self._previous_line = -1
        self.scrollable = True
        self._job_runner = DelayJobRunner(delay=100)
        self.setMouseTracking(True)
        self._to_remove = []

    @property
    def markers(self):
        """
        Gets all markers.
        """
        return self._markers

    def add_marker(self, marker):
        """
        Adds the marker to the panel.

        :param marker: Marker to add
        :type marker: pyqode.core.modes.Marker
        """
        self._markers.append(marker)
        doc = self.editor.document()
        assert isinstance(doc, QtGui.QTextDocument)
        block = doc.findBlockByLineNumber(marker._position)
        marker.block = block
        d = TextDecoration(block)
        d.set_full_width()
        if self._background:
            d.set_background(QtGui.QBrush(self._background))
            marker.decoration = d
        self.editor.decorations.append(d)
        self.repaint()

    def remove_marker(self, marker):
        """
        Removes a marker from the panel

        :param marker: Marker to remove
        :type marker: pyqode.core.Marker
        """
        self._markers.remove(marker)
        self._to_remove.append(marker)
        if hasattr(marker, 'decoration'):
            self.editor.decorations.remove(marker.decoration)
        self.repaint()

    def clear_markers(self):
        """ Clears the markers list """
        while len(self._markers):
            self.remove_marker(self._markers[0])

    def marker_for_line(self, line):
        """
        Returns the marker that is displayed at the specified line number if
        any.

        :param line: The marker line.

        :return: Marker of None
        :rtype: pyqode.core.Marker
        """
        markers = []
        for marker in self._markers:
            if line == marker.position:
                markers.append(marker)
        return markers

    def sizeHint(self):
        """
        Returns the panel size hint. (fixed with of 16px)
        """
        metrics = QtGui.QFontMetricsF(self.editor.font())
        size_hint = QtCore.QSize(metrics.height(), metrics.height())
        if size_hint.width() > 16:
            size_hint.setWidth(16)
        return size_hint

    def paintEvent(self, event):
        Panel.paintEvent(self, event)
        painter = QtGui.QPainter(self)
        for top, block_nbr, block in self.editor.visible_blocks:
            for marker in self._markers:
                if marker.block == block and marker.icon:
                    rect = QtCore.QRect()
                    rect.setX(0)
                    rect.setY(top)
                    rect.setWidth(self.sizeHint().width())
                    rect.setHeight(self.sizeHint().height())
                    marker.icon.paint(painter, rect)

    def mousePressEvent(self, event):
        # Handle mouse press:
        # - emit add marker signal if there were no marker under the mouse
        #   cursor
        # - emit remove marker signal if there were one or more markers under
        #   the mouse cursor.
        line = TextHelper(self.editor).line_nbr_from_position(event.pos().y())
        if self.marker_for_line(line):
            if event.button() == QtCore.Qt.LeftButton:
                self.remove_marker_requested.emit(line)
            else:
                self.edit_marker_requested.emit(line)
        else:
            self.add_marker_requested.emit(line)

    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

    def leaveEvent(self, *args, **kwargs):
        """
        Hide tooltip when leaving the panel region.
        """
        QtWidgets.QToolTip.hideText()
        self._previous_line = -1

    def _display_tooltip(self, tooltip, top):
        """
        Display tooltip at the specified top position.
        """
        QtWidgets.QToolTip.showText(self.mapToGlobal(QtCore.QPoint(
            self.sizeHint().width(), top)), tooltip, self)
Пример #18
0
    def __init__(self, parent=None, create_default_actions=True):
        """
        :param parent: Parent widget

        :param create_default_actions: True to create the action for the
            standard shortcuts (copy, paste, delete, undo, redo,...).
            Non-standard actions will always get created. If you would like
            to prevent the context menu from showing, just set the
            :attr:`show_menu_enabled` to False.
        """
        super(CodeEdit, self).__init__(parent)
        self.installEventFilter(self)
        self.clones = []
        self._show_ctx_mnu = True
        self._default_font_size = 10
        self._backend = BackendManager(self)
        self._file = FileManager(self)
        self._modes = ModesManager(self)
        self._panels = PanelsManager(self)
        self._decorations = TextDecorationsManager(self)
        self.document().modificationChanged.connect(self._emit_dirty_changed)

        self._word_separators = [
            "~",
            "!",
            "@",
            "#",
            "$",
            "%",
            "^",
            "&",
            "*",
            "(",
            ")",
            "+",
            "{",
            "}",
            "|",
            ":",
            '"',
            "'",
            "<",
            ">",
            "?",
            ",",
            ".",
            "/",
            ";",
            "[",
            "]",
            "\\",
            "\n",
            "\t",
            "=",
            "-",
            " ",
        ]
        self._save_on_focus_out = False
        self._use_spaces_instead_of_tabs = True
        self._whitespaces_foreground = None
        self._sel_background = None
        self._show_whitespaces = False
        self._foreground = None
        self._sel_foreground = None
        self._tab_length = 4
        self._zoom_level = 0
        self._font_size = 10
        self._background = None
        QtGui.QFontDatabase.addApplicationFont(":/fonts/rc/SourceCodePro-Regular.ttf")
        QtGui.QFontDatabase.addApplicationFont(":/fonts/rc/SourceCodePro-Bold.ttf")
        self._font_family = self._DEFAULT_FONT
        self._mimetypes = []
        self._select_line_on_copy_empty = True

        # Flags/Working variables
        self._last_mouse_pos = QtCore.QPoint(0, 0)
        self._modified_lines = set()
        self._cleaning = False
        self._visible_blocks = []
        self._tooltips_runner = DelayJobRunner(delay=700)
        self._prev_tooltip_block_nbr = -1
        self._original_text = ""

        self._dirty = False

        # setup context menu
        self._actions = []
        self._menus = []
        self._init_actions(create_default_actions)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._show_context_menu)
        self._mnu = None  # bug with PySide (github #63)

        # init settings and styles from global settings/style modules
        self._init_settings()
        self._init_style()

        # connect slots
        self.textChanged.connect(self._on_text_changed)
        self.blockCountChanged.connect(self.update)
        self.cursorPositionChanged.connect(self.update)
        self.selectionChanged.connect(self.update)

        self.setMouseTracking(True)
        self.setCenterOnScroll(True)
        self.setLineWrapMode(self.NoWrap)
        self.setCursorWidth(2)
Пример #19
0
    def __init__(self, parent=None, create_default_actions=True):
        """
        :param parent: Parent widget

        :param create_default_actions: True to create the default context
            menu actions (copy, paste, edit)
        """
        super(CodeEdit, self).__init__(parent)
        self.clones = []
        self._default_font_size = 10
        self._backend = BackendManager(self)
        self._file = FileManager(self)
        self._modes = ModesManager(self)
        self._panels = PanelsManager(self)
        self._decorations = TextDecorationsManager(self)
        self.document().modificationChanged.connect(self._emit_dirty_changed)

        self._word_separators = [
            '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '{',
            '}', '|', ':', '"', "'", "<", ">", "?", ",", ".", "/", ";", '[',
            ']', '\\', '\n', '\t', '=', '-', ' '
        ]
        self._save_on_focus_out = False
        self._use_spaces_instead_of_tabs = True
        self._whitespaces_foreground = None
        self._sel_background = None
        self._show_whitespaces = False
        self._foreground = None
        self._sel_foreground = None
        self._tab_length = 4
        self._zoom_level = 0
        self._font_size = 10
        self._background = None
        QtGui.QFontDatabase.addApplicationFont(
            ':/fonts/rc/SourceCodePro-Regular.ttf')
        QtGui.QFontDatabase.addApplicationFont(
            ':/fonts/rc/SourceCodePro-Bold.ttf')
        self._font_family = self._DEFAULT_FONT
        self._mimetypes = []

        # Flags/Working variables
        self._last_mouse_pos = QtCore.QPoint(0, 0)
        self._modified_lines = set()
        self._cleaning = False
        self._visible_blocks = []
        self._tooltips_runner = DelayJobRunner(delay=700)
        self._prev_tooltip_block_nbr = -1
        self._original_text = ""

        self._dirty = False

        # setup context menu
        self._actions = []
        self._menus = []
        if create_default_actions:
            self._init_actions()
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._show_context_menu)
        self._mnu = None  # bug with PySide (github #63)

        # init settings and styles from global settings/style modules
        self._init_settings()
        self._init_style()

        # connect slots
        self.textChanged.connect(self._on_text_changed)
        self.blockCountChanged.connect(self.update)
        self.cursorPositionChanged.connect(self.update)
        self.selectionChanged.connect(self.update)

        self.setMouseTracking(True)
        self.setCenterOnScroll(True)
        self.setLineWrapMode(self.NoWrap)
Пример #20
0
class CheckerMode(Mode, QtCore.QObject):
    """
    Performs a user defined code analysis job using the backend and
    display the results on the editor instance.

    The user defined code analysis job is a simple **function** with the
    following signature:

    .. code-block:: python

        def analysisProcess(data)

    where data is the request data:

    .. code-block:: python

        request_data = {
                'code': self.editor.toPlainText(),
                'path': self.editor.file.path,
                'encoding': self.editor.file.encoding
            }

    and the return value is a tuple made up of the following elements:

        (description, status, line, [col], [icon], [color], [path])

    The background process is ran when the text changed and the ide is an idle
    state for a few seconds.

    You can also request an analysis manually using
    :meth:`pyqode.core.modes.CheckerMode.request_analysis`

    Messages are displayed as text decorations on the editor. A checker panel
    will take care of display message icons next to each line.
    """
    @property
    def messages(self):
        """
        Returns the entire list of checker messages.
        """
        return self._messages

    def __init__(self, worker, delay=500, show_tooltip=True):
        """
        :param worker: The process function or class to call remotely.
        :param delay: The delay used before running the analysis process when
                      trigger is set to
                      :class:pyqode.core.modes.CheckerTriggers`
        :param show_tooltip: Specify if a tooltip must be displayed when the
                             mouse is over a checker message decoration.
        """
        Mode.__init__(self)
        QtCore.QObject.__init__(self)
        # max number of messages to keep good performances
        self.limit = 200
        self.ignore_rules = []
        self._job_runner = DelayJobRunner(delay=delay)
        self._messages = []
        self._worker = worker
        self._mutex = QtCore.QMutex()
        self._show_tooltip = show_tooltip
        self._pending_msg = []
        self._finished = True

    def set_ignore_rules(self, rules):
        """
        Sets the ignore rules for the linter.

        Rules are a list of string that the actual linter function will check
        to reject some warnings/errors.
        """
        self.ignore_rules = rules

    def add_messages(self, messages):
        """
        Adds a message or a list of message.

        :param messages: A list of messages or a single message
        """
        # remove old messages
        if len(messages) > self.limit:
            messages = messages[:self.limit]
        _logger(self.__class__).log(5, 'adding %s messages' % len(messages))
        self._finished = False
        self._new_messages = messages
        self._to_check = list(self._messages)
        self._pending_msg = messages
        # start removing messages, new message won't be added until we
        # checked all message that need to be removed
        QtCore.QTimer.singleShot(1, self._remove_batch)

    def _remove_batch(self):
        if self.editor is None:
            return
        for i in range(100):
            if not len(self._to_check):
                # all messages checker, start adding messages now
                QtCore.QTimer.singleShot(1, self._add_batch)
                self.editor.repaint()
                return False
            msg = self._to_check.pop(0)
            if msg.block is None:
                msg.block = self.editor.document().findBlockByNumber(msg.line)
            if msg not in self._new_messages:
                self.remove_message(msg)
        self.editor.repaint()
        QtCore.QTimer.singleShot(1, self._remove_batch)

    def _add_batch(self):
        if self.editor is None:
            return
        for i in range(10):
            if not len(self._pending_msg):
                # all pending message added
                self._finished = True
                _logger(self.__class__).log(5, 'finished')
                self.editor.repaint()
                return False
            message = self._pending_msg.pop(0)
            if message.line >= 0:
                try:
                    usd = message.block.userData()
                except AttributeError:
                    message.block = self.editor.document().findBlockByNumber(
                        message.line)
                    usd = message.block.userData()
                if usd is None:
                    usd = TextBlockUserData()
                    message.block.setUserData(usd)
                # check if the same message already exists
                if message in usd.messages:
                    continue
                self._messages.append(message)
                usd.messages.append(message)
                tooltip = None
                if self._show_tooltip:
                    tooltip = message.description
                message.decoration = TextDecoration(self.editor.textCursor(),
                                                    start_line=message.line,
                                                    tooltip=tooltip,
                                                    draw_order=3)
                message.decoration.set_full_width()
                message.decoration.set_as_error(
                    color=QtGui.QColor(message.color))
                self.editor.decorations.append(message.decoration)
        QtCore.QTimer.singleShot(1, self._add_batch)
        self.editor.repaint()
        return True

    def remove_message(self, message):
        """
        Removes a message.

        :param message: Message to remove
        """
        import time
        _logger(self.__class__).log(5, 'removing message %s' % message)
        t = time.time()
        usd = message.block.userData()
        if usd:
            try:
                usd.messages.remove(message)
            except (AttributeError, ValueError):
                pass
        if message.decoration:
            self.editor.decorations.remove(message.decoration)
        self._messages.remove(message)

    def clear_messages(self):
        """
        Clears all messages.
        """
        while len(self._messages):
            msg = self._messages.pop(0)
            usd = msg.block.userData()
            if usd and hasattr(usd, 'messages'):
                usd.messages[:] = []
            if msg.decoration:
                self.editor.decorations.remove(msg.decoration)

    def on_state_changed(self, state):
        if state:
            self.editor.textChanged.connect(self.request_analysis)
            self.editor.new_text_set.connect(self.clear_messages)
            self.request_analysis()
        else:
            self.editor.textChanged.disconnect(self.request_analysis)
            self.editor.new_text_set.disconnect(self.clear_messages)
            self._job_runner.cancel_requests()
            self.clear_messages()

    def _on_work_finished(self, results):
        """
        Display results.

        :param status: Response status
        :param results: Response data, messages.
        """
        messages = []
        for msg in results:
            msg = CheckerMessage(*msg)
            if msg.line >= self.editor.blockCount():
                msg.line = self.editor.blockCount() - 1
            block = self.editor.document().findBlockByNumber(msg.line)
            msg.block = block
            messages.append(msg)
        self.add_messages(messages)

    def request_analysis(self):
        """
        Requests an analysis.
        """
        if self._finished:
            _logger(self.__class__).log(5, 'running analysis')
            self._job_runner.request_job(self._request)
        elif self.editor:
            # retry later
            _logger(self.__class__).log(
                5, 'delaying analysis (previous analysis not finished)')
            QtCore.QTimer.singleShot(500, self.request_analysis)

    def _request(self):
        """ Requests a checking of the editor content. """
        try:
            self.editor.toPlainText()
        except (TypeError, RuntimeError):
            return
        try:
            max_line_length = self.editor.modes.get('RightMarginMode').position
        except KeyError:
            max_line_length = 79
        request_data = {
            'code': self.editor.toPlainText(),
            'path': self.editor.file.path,
            'encoding': self.editor.file.encoding,
            'ignore_rules': self.ignore_rules,
            'max_line_length': max_line_length,
        }
        try:
            self.editor.backend.send_request(self._worker,
                                             request_data,
                                             on_receive=self._on_work_finished)
            self._finished = False
        except NotRunning:
            # retry later
            QtCore.QTimer.singleShot(100, self._request)