def test_selected_text(editor): helper = TextHelper(editor) helper.goto_line(2, 1, move=True) QTest.qWait(100) assert helper.word_under_cursor().selectedText() == 'T' assert helper.word_under_cursor( select_whole_word=True).selectedText() == 'This'
def _on_key_pressed(self, event): """ Override key press to select the current scope if the user wants to deleted a folded scope (without selecting it). """ delete_request = event.key() in [QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete] if event.text() or delete_request: cursor = self.editor.textCursor() if cursor.hasSelection(): # change selection to encompass the whole scope. positions_to_check = cursor.selectionStart(), cursor.selectionEnd() else: positions_to_check = (cursor.position(), ) for pos in positions_to_check: block = self.editor.document().findBlock(pos) th = TextBlockHelper() if th.is_fold_trigger(block) and th.is_collapsed(block): self.toggle_fold_trigger(block) if delete_request and cursor.hasSelection(): scope = FoldScope(self.find_parent_scope(block)) tc = TextHelper(self.editor).select_lines(*scope.get_range()) if tc.selectionStart() > cursor.selectionStart(): start = cursor.selectionStart() else: start = tc.selectionStart() if tc.selectionEnd() < cursor.selectionEnd(): end = cursor.selectionEnd() else: end = tc.selectionEnd() tc.setPosition(start) tc.setPosition(end, tc.KeepAnchor) self.editor.setTextCursor(tc)
def request_completion(self): """ Requests a code completion at the current cursor position. """ _logger().debug('request code completion') self._col = self.editor.textCursor().positionInBlock() - len( self.completion_prefix) helper = TextHelper(self.editor) if not self._request_cnt: # only check first byte tc = self.editor.textCursor() while tc.atBlockEnd() and not tc.atBlockStart() and \ tc.position(): tc.movePosition(tc.Left) disabled_zone = TextHelper(self.editor).is_comment_or_string( tc) if disabled_zone: _logger().debug( "cc: cancel request, cursor is in a disabled zone") return False self._request_cnt += 1 self._collect_completions(self.editor.toPlainText(), helper.current_line_nbr(), helper.current_column_nbr() - len(self.completion_prefix), self.editor.file.path, self.editor.file.encoding, self.completion_prefix) return True return False
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 test_clear_selection(editor): editor.file.open(__file__) helper = TextHelper(editor) TextHelper(editor).select_lines(0, 2) assert helper.selected_text() != '' TextHelper(editor).clear_selection() assert helper.selected_text() == ''
def test_line_indent(editor): editor.setPlainText(src, 'text/x-python', 'utf-8') assert TextHelper(editor).line_indent(0) == 0 assert TextHelper(editor).line_indent(1) == 4 editor.file.open(__file__) assert TextHelper(editor).line_indent(TextHelper(editor).line_count() - 2) == 4
def test_line_nbr_from_position(editor): editor.repaint() sys.stderr.write(str(editor.visible_blocks)) assert TextHelper(editor).line_nbr_from_position( TextHelper(editor).line_pos_from_number(0)) is not None assert TextHelper(editor).line_nbr_from_position(-1) == -1 QTest.qWait(100)
def paintEvent(self, event): # Paints the line numbers self._line_color_u = drift_color(self._background_brush.color(), 250) self._line_color_s = drift_color(self._background_brush.color(), 280) Panel.paintEvent(self, event) if self.isVisible(): painter = QtGui.QPainter(self) # get style options (font, size) width = self.width() height = self.editor.fontMetrics().height() font = self.editor.font() bold_font = self.editor.font() bold_font.setBold(True) pen = QtGui.QPen(self._line_color_u) pen_selected = QtGui.QPen(self._line_color_s) painter.setFont(font) # get selection range sel_start, sel_end = TextHelper(self.editor).selection_range() has_sel = sel_start != sel_end cl = TextHelper(self.editor).current_line_nbr() # draw every visible blocks for top, line, block in self.editor.visible_blocks: if ((has_sel and sel_start <= line <= sel_end) or (not has_sel and cl == line)): painter.setPen(pen_selected) painter.setFont(bold_font) else: painter.setPen(pen) painter.setFont(font) painter.drawText(-3, top, width, height, QtCore.Qt.AlignRight, str(line + 1))
def mouseMoveEvent(self, e): # Updates end of selection if we are currently selecting if self._selecting: end_pos = e.pos().y() start_line = TextHelper(self.editor).line_nbr_from_position( self._sel_start) end_line = TextHelper(self.editor).line_nbr_from_position(end_pos) TextHelper(self.editor).select_lines(start_line, end_line)
def test_line_pos_from_number(editor): assert TextHelper(editor).line_pos_from_number(0) is not None # out of range line will return the bottom of the document or the top assert TextHelper(editor).line_pos_from_number(-1) == 0 pos = TextHelper(editor).line_pos_from_number( TextHelper(editor).line_count() + 10) assert pos is not None assert pos > 0
def test_select_lines(editor): TextHelper(editor).select_lines(0, 4) QTest.qWait(100) QTest.qWait(1000) assert TextHelper(editor).selection_range() == (0, 4) TextHelper(editor).select_lines(-1, 10) assert TextHelper(editor).selection_range() == (0, 10) editor.decorations.clear()
def mousePressEvent(self, e): """ Starts selecting """ self._selecting = True self._sel_start = e.pos().y() start = end = TextHelper(self.editor).line_nbr_from_position( self._sel_start) TextHelper(self.editor).select_lines(start, end)
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)
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 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)
def test_keep_tc(editor): @keep_tc_pos def move_cursor(editor, arg): assert arg == 'arg' TextHelper(editor).goto_line(4) l, c = TextHelper(editor).cursor_position() move_cursor(editor, 'arg') nl, nc = TextHelper(editor).cursor_position() assert l == nl and c == nc
def test_do_home_key(editor): QTest.qWait(2000) helper = TextHelper(editor) helper.goto_line(336, 29) assert editor.textCursor().positionInBlock() == 29 assert TextHelper(editor).line_indent() == 4 editor._do_home_key() assert editor.textCursor().positionInBlock() == 4 editor._do_home_key() assert editor.textCursor().positionInBlock() == 0
def test_cut_no_selection(editor): assert isinstance(editor, CodeEdit) editor.setPlainText('''Line 1 Line 2 Line 3''', '', '') helper = TextHelper(editor) # eat empty line helper.goto_line(0) assert helper.line_count() == 3 editor.cut() assert helper.line_count() == 2
def test_goto_line(editor): assert editor.textCursor().blockNumber() == 0 assert editor.textCursor().columnNumber() == 0 cursor = TextHelper(editor).goto_line(2, 0, move=False) QTest.qWait(100) assert editor.textCursor().blockNumber() != cursor.blockNumber() assert editor.textCursor().columnNumber() == cursor.columnNumber() cursor = TextHelper(editor).goto_line(9, move=True) QTest.qWait(100) assert editor.textCursor().blockNumber() == cursor.blockNumber() == 9 assert editor.textCursor().columnNumber() == cursor.columnNumber() == 0 assert TextHelper(editor).current_line_nbr() == 9 assert TextHelper(editor).current_column_nbr() == 0
def mouseMoveEvent(self, e): # Updates end of selection if we are currently selecting if self._selecting: end_pos = e.pos().y() end_line = TextHelper(self.editor).line_nbr_from_position(end_pos) if end_line == -1 and self.editor.visible_blocks: # take last visible block if end_pos < 50: _, end_line, _ = self.editor.visible_blocks[0] end_line -= 1 else: _, end_line, _ = self.editor.visible_blocks[-1] end_line += 1 TextHelper(self.editor).select_lines(self._start_line, end_line)
def provide_ide_current_word(self): editor = self._current_editor() if editor is None: return u'' return TextHelper(editor).word_under_cursor( select_whole_word=True).selectedText()
def _do_home_key(self, event=None, select=False): """ Performs home key action """ cursor = self.textCursor() move = QtGui.QTextCursor.MoveAnchor if select: move = QtGui.QTextCursor.KeepAnchor indent = TextHelper(self).line_indent() # Scenario 1: We're on an unindented block. In that case, we jump back # to the start of the visible line, but not all the way to the back of # the block. This is what you would expect when working with text and # line wrapping is enabled. if not indent: cursor.movePosition(QtGui.QTextCursor.StartOfLine, move) else: delta = self.textCursor().positionInBlock() - indent # Scenario 2: We're on an indented block. In that case, we move # back to the indented position. This is what you would expect when # working with code. if delta > 0: cursor.movePosition(QtGui.QTextCursor.Left, move, delta) # Scenario 3: We're on an indented block, but we're already at the # start of the indentation. In that case, we jump back to the # beginning of the block. else: cursor.movePosition(QtGui.QTextCursor.StartOfBlock, move) self.setTextCursor(cursor) if event: event.accept()
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 cut(self): tc = self.textCursor() helper = TextHelper(self) tc.beginEditBlock() no_selection = False if not helper.current_line_text().strip(): tc.deleteChar() else: if not self.textCursor().hasSelection(): no_selection = True TextHelper(self).select_whole_line() super(CodeEdit, self).cut() if no_selection: tc.deleteChar() tc.endEditBlock() self.setTextCursor(tc)
def test_indent(editor): # disable indenter mode -> indent should not do anything editor.modes.get(modes.IndenterMode).enabled = False TextHelper(editor).goto_line(0, move=True) first_line = get_first_line(editor) editor.indent() assert get_first_line(editor) == first_line editor.un_indent() assert get_first_line(editor) == first_line # enable indenter mode, call to indent/un_indent should now work editor.modes.get(modes.IndenterMode).enabled = True TextHelper(editor).goto_line(0) editor.indent() assert get_first_line(editor) == editor.tab_length * ' ' + first_line editor.un_indent() assert get_first_line(editor) == first_line
def __swapLine(self, up: bool): helper = TextHelper(self) text = helper.current_line_text() line_nbr = helper.current_line_nbr() if up: swap_line_nbr = line_nbr - 1 else: swap_line_nbr = line_nbr + 1 swap_text = helper.line_text(swap_line_nbr) if (swap_line_nbr < helper.line_count() and line_nbr < helper.line_count()): helper.set_line_text(line_nbr, swap_text) helper.set_line_text(swap_line_nbr, text)
def copy(self): """ Copy the selected text to the clipboard. If no text was selected, the entire line is copied (this feature can be turned off by setting :attr:`select_line_on_copy_empty` to False. """ if self.select_line_on_copy_empty and not self.textCursor().hasSelection(): TextHelper(self).select_whole_line() super(CodeEdit, self).copy()
def zoom_in(self, increment=1): """ Zooms in the editor (makes the font bigger). :param increment: zoom level increment. Default is 1. """ self.zoom_level += increment TextHelper(self).mark_whole_doc_dirty() self._reset_stylesheet()
def test_duplicate_line(editor): QTest.qWait(1000) TextHelper(editor).goto_line(0) editor.duplicate_line() assert editor.toPlainText().startswith(get_first_line(editor) + '\n' + get_first_line(editor)) editor.setPlainText('foo', '', 'utf-8') editor.duplicate_line() assert editor.toPlainText() == 'foo\nfoo' assert editor.textCursor().position() == 7
def test_clean_document(editor): TextHelper(editor).clean_document() count = TextHelper(editor).line_count() TextHelper(editor).set_line_text(0, '""" ') editor.appendPlainText("") editor.appendPlainText("") editor.appendPlainText("") assert TextHelper(editor).line_count() == count + 3 TextHelper(editor).select_lines(0, TextHelper(editor).line_count()) TextHelper(editor).clean_document() QTest.qWait(100) assert TextHelper(editor).line_count() == count
def cut(self): """ Cuts the selected text or the whole line if no text was selected. """ tc = self.textCursor() helper = TextHelper(self) tc.beginEditBlock() no_selection = False sText = tc.selection().toPlainText() if not helper.current_line_text() and sText.count("\n") > 1: tc.deleteChar() else: if not self.textCursor().hasSelection(): no_selection = True TextHelper(self).select_whole_line() super(CodeEdit, self).cut() if no_selection: tc.deleteChar() tc.endEditBlock() self.setTextCursor(tc)
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 _update(self, rect, delta_y, force_update_margins=False): """ Updates panels """ helper = TextHelper(self.editor) if not self: return for zones_id, zone in self._panels.items(): if zones_id == Panel.Position.TOP or \ zones_id == Panel.Position.BOTTOM: continue panels = list(zone.values()) for panel in panels: if panel.scrollable and delta_y: panel.scroll(0, delta_y) line, col = helper.cursor_position() oline, ocol = self._cached_cursor_pos if line != oline or col != ocol or panel.scrollable: panel.update(0, rect.y(), panel.width(), rect.height()) self._cached_cursor_pos = helper.cursor_position() if (rect.contains(self.editor.viewport().rect()) or force_update_margins): self._update_viewport_margins()
def test_copy_no_selection(editor): """ Tests the select_line_on_copy_empty option that toggles the "whole line selection on copy with empty selection"-feature """ assert isinstance(editor, CodeEdit) editor.setPlainText('''Line 1 Line 2 Line 3''', '', '') helper = TextHelper(editor) helper.goto_line(0) editor.textCursor().clearSelection() editor.select_line_on_copy_empty = False editor.copy() assert editor.textCursor().hasSelection() is False editor.textCursor().clearSelection() editor.select_line_on_copy_empty = True editor.copy() assert editor.textCursor().hasSelection()
def zoom_in(self, increment=1): """ Zooms in the editor (makes the font bigger). :param increment: zoom level increment. Default is 1. """ # When called through an action, the first argument is a bool if isinstance(increment, bool): increment = 1 self.zoom_level += increment TextHelper(self).mark_whole_doc_dirty() self._reset_stylesheet()
def _draw_fold_region_background(self, block, painter): """ Draw the fold region when the mouse is over and non collapsed indicator. :param top: Top position :param block: Current block. :param painter: QPainter """ r = folding.FoldScope(block) th = TextHelper(self.editor) start, end = r.get_range(ignore_blank_lines=True) if start > 0: top = th.line_pos_from_number(start) else: top = 0 bottom = th.line_pos_from_number(end + 1) h = bottom - top if h == 0: h = self.sizeHint().height() w = self.sizeHint().width() self._draw_rect(QtCore.QRectF(0, top, w, h), painter)
def mouseMoveEvent(self, event): """ Detect mouser over indicator and highlight the current scope in the editor (up and down decoration arround the foldable text when the mouse is over an indicator). :param event: event """ super(FoldingPanel, self).mouseMoveEvent(event) th = TextHelper(self.editor) line = th.line_nbr_from_position(event.pos().y()) if line >= 0: block = FoldScope.find_parent_scope( self.editor.document().findBlockByNumber(line)) if TextBlockHelper.is_fold_trigger(block): if self._mouse_over_line is None: # mouse enter fold scope QtWidgets.QApplication.setOverrideCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) if self._mouse_over_line != block.blockNumber() and \ self._mouse_over_line is not None: # fold scope changed, a previous block was highlighter so # we quickly update our highlighting self._mouse_over_line = block.blockNumber() self._highlight_surrounding_scopes(block) else: # same fold scope, request highlight self._mouse_over_line = block.blockNumber() self._highlight_runner.request_job( self._highlight_surrounding_scopes, block) self._highight_block = block else: # no fold scope to highlight, cancel any pending requests self._highlight_runner.cancel_requests() self._mouse_over_line = None QtWidgets.QApplication.restoreOverrideCursor() self.repaint()
def test_matched_selection(editor): line, column, text = 233, 14, ''' editor.textCursor(), 'import', QtGui.QTextDocument.FindCaseSensitively''' cursor = editor.textCursor() assert not cursor.hasSelection() helper = TextHelper(editor) helper.goto_line(line, column) assert helper.cursor_position()[0] == line assert helper.cursor_position()[1] == column cursor = editor.textCursor() helper.match_select() cursor = editor.textCursor() assert cursor.hasSelection() assert text in cursor.selectedText()
def test_matched_selection(editor): line, column, text = 297, 14, '''__file__''' cursor = editor.textCursor() assert not cursor.hasSelection() helper = TextHelper(editor) helper.goto_line(line, column) assert helper.cursor_position()[0] == line assert helper.cursor_position()[1] == column cursor = editor.textCursor() helper.match_select() cursor = editor.textCursor() assert cursor.hasSelection() assert text in cursor.selectedText()
def test_extended_selection(editor): for line, column, text in [(8, 15, 'pyqode.core.api.utils'), (8, 1, 'from')]: editor.file.open(__file__) QTest.qWait(1000) cursor = editor.textCursor() assert not cursor.hasSelection() helper = TextHelper(editor) helper.goto_line(line, column) assert helper.cursor_position()[0] == line assert helper.cursor_position()[1] == column cursor = editor.textCursor() assert text in cursor.block().text() helper.select_extended_word() cursor = editor.textCursor() assert cursor.hasSelection() assert cursor.selectedText() == text
class CodeCompletionMode(Mode, QtCore.QObject): """ Provides code completions when typing or when pressing Ctrl+Space. This mode provides a code completion system which is extensible. µ It takes care of running the completion request in a background process using one or more completion provider. To implement a code completion for a specific language, you only need to implement new :class:`pyqode.core.backend.workers.CodeCompletionWorker.Provider` The completion popup is shown the user press **ctrl+space** or automatically while the user is typing some code (this can be configured using a series of properties). """ @property def trigger_key(self): """ The key that triggers code completion (Default is **Space**: Ctrl + Space). """ return self._trigger_key @trigger_key.setter def trigger_key(self, value): self._trigger_key = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).trigger_key = value except KeyError: # this should never happen since we're working with clones pass @property def trigger_length(self): """ The trigger length defines the word length required to run code completion. """ return self._trigger_len @trigger_length.setter def trigger_length(self, value): self._trigger_len = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).trigger_length = value except KeyError: # this should never happen since we're working with clones pass @property def trigger_symbols(self): """ Defines the list of symbols that immediately trigger a code completion requiest. BY default, this list contains the dot character. For C++, we would add the '->' operator to that list. """ return self._trigger_symbols @trigger_symbols.setter def trigger_symbols(self, value): self._trigger_symbols = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).trigger_symbols = value except KeyError: # this should never happen since we're working with clones pass @property def show_tooltips(self): """ True to show tooltips next to the current completion. """ return self._show_tooltips @show_tooltips.setter def show_tooltips(self, value): self._show_tooltips = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).show_tooltips = value except KeyError: # this should never happen since we're working with clones pass @property def case_sensitive(self): """ True to performs case sensitive completion matching. """ return self._case_sensitive @case_sensitive.setter def case_sensitive(self, value): self._case_sensitive = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).case_sensitive = value except KeyError: # this should never happen since we're working with clones pass @property def completion_prefix(self): """ Returns the current completion prefix """ return self._helper.word_under_cursor( select_whole_word=False).selectedText().strip() def __init__(self): Mode.__init__(self) QtCore.QObject.__init__(self) self._current_completion = "" # use to display a waiting cursor if completion provider takes too much # time self._job_runner = DelayJobRunner(delay=1000) self._tooltips = {} self._cursor_line = -1 self._cancel_next = False self._request_cnt = 0 self._last_completion_prefix = "" self._trigger_key = None self._trigger_len = None self._trigger_symbols = None self._show_tooltips = None self._case_sensitive = None self._data = None self._completer = None self._col = 0 self._skip_next_backspace_released = False self._init_settings() def _init_settings(self): self._trigger_key = QtCore.Qt.Key_Space self._trigger_len = 1 self._trigger_symbols = ['.'] self._show_tooltips = True self._case_sensitive = False def request_completion(self): """ Requests a code completion at the current cursor position. """ _logger().debug('request code completion') self._col = self.editor.textCursor().positionInBlock() - len( self.completion_prefix) helper = TextHelper(self.editor) if not self._request_cnt: # only check first byte tc = self.editor.textCursor() while tc.atBlockEnd() and not tc.atBlockStart() and \ tc.position(): tc.movePosition(tc.Left) disabled_zone = TextHelper(self.editor).is_comment_or_string( tc) if disabled_zone: _logger().debug( "cc: cancel request, cursor is in a disabled zone") return False self._request_cnt += 1 self._collect_completions(self.editor.toPlainText(), helper.current_line_nbr(), helper.current_column_nbr() - len(self.completion_prefix), self.editor.file.path, self.editor.file.encoding, self.completion_prefix) return True return False def on_install(self, editor): self._completer = QtWidgets.QCompleter([""], editor) self._completer.setCompletionMode(self._completer.PopupCompletion) self._completer.activated.connect(self._insert_completion) self._completer.highlighted.connect( self._on_selected_completion_changed) self._completer.setModel(QtGui.QStandardItemModel()) self._helper = TextHelper(editor) Mode.on_install(self, editor) def on_uninstall(self): Mode.on_uninstall(self) self._completer.popup().hide() self._completer = None def on_state_changed(self, state): if state: self.editor.focused_in.connect(self._on_focus_in) self.editor.key_pressed.connect(self._on_key_pressed) self.editor.post_key_pressed.connect(self._on_key_released) self._completer.highlighted.connect( self._display_completion_tooltip) self.editor.cursorPositionChanged.connect( self._on_cursor_position_changed) else: self.editor.focused_in.disconnect(self._on_focus_in) self.editor.key_pressed.disconnect(self._on_key_pressed) self.editor.post_key_pressed.disconnect(self._on_key_released) self._completer.highlighted.disconnect( self._display_completion_tooltip) self.editor.cursorPositionChanged.disconnect( self._on_cursor_position_changed) def _on_focus_in(self, event): """ Resets completer widget :param event: QFocusEvents """ self._completer.setWidget(self.editor) def _on_results_available(self, results): _logger().debug("cc: got completion results") if self.editor: # self.editor.set_mouse_cursor(QtCore.Qt.IBeamCursor) all_results = [] for res in results: all_results += res self._request_cnt -= 1 self._show_completions(all_results) def _on_key_pressed(self, event): is_shortcut = self._is_shortcut(event) # handle completer popup events ourselves if self._completer.popup().isVisible(): self._handle_completer_events(event) if is_shortcut: event.accept() if is_shortcut: self.request_completion() event.accept() @staticmethod def _is_navigation_key(event): return (event.key() == QtCore.Qt.Key_Backspace or event.key() == QtCore.Qt.Key_Back or event.key() == QtCore.Qt.Key_Delete or event.key() == QtCore.Qt.Key_Left or event.key() == QtCore.Qt.Key_Right or event.key() == QtCore.Qt.Key_Up or event.key() == QtCore.Qt.Key_Down or event.key() == QtCore.Qt.Key_Space) @staticmethod def _is_end_of_word_char(event, is_printable, symbols, seps): ret_val = False if is_printable and symbols: k = event.text() ret_val = (k in seps and k not in symbols) return ret_val def _update_prefix(self, event, is_end_of_word, is_navigation_key): self._completer.setCompletionPrefix(self.completion_prefix) cnt = self._completer.completionCount() n = len(self.editor.textCursor().block().text()) c = self.editor.textCursor().positionInBlock() if (not cnt or ((self.completion_prefix == "" and n == 0) and is_navigation_key) or is_end_of_word or c < self._col or (int(event.modifiers()) and event.key() == QtCore.Qt.Key_Backspace)): self._hide_popup() else: self._show_popup() def _on_key_released(self, event): if (event.key() == QtCore.Qt.Key_Backspace and self._skip_next_backspace_released): self._skip_next_backspace_released = False return if self._is_shortcut(event): return if (event.key() == QtCore.Qt.Key_Home or event.key() == QtCore.Qt.Key_End or event.key() == QtCore.Qt.Key_Shift): return is_printable = self._is_printable_key_event(event) is_navigation_key = self._is_navigation_key(event) symbols = self._trigger_symbols is_end_of_word = self._is_end_of_word_char( event, is_printable, symbols, self.editor.word_separators) cursor = self._helper.word_under_cursor() cpos = cursor.position() cursor.setPosition(cpos) cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor) text_to_cursor = cursor.selectedText() cursor.setPosition(cpos) cursor.movePosition(cursor.EndOfLine, cursor.KeepAnchor) text_after_cursor = cursor.selectedText() if self._completer.popup().isVisible(): # hide popup if the user is moving the cursor out of the current # word boundaries. if (len(text_after_cursor) and is_navigation_key and text_after_cursor[0] in self.editor.word_separators and not text_after_cursor[0].isspace()): self._hide_popup() else: self._update_prefix(event, is_end_of_word, is_navigation_key) if is_printable: if event.text() == " ": self._cancel_next = self._request_cnt return else: # trigger symbols if symbols: for symbol in symbols: if text_to_cursor.endswith(symbol): _logger().debug("cc: symbols trigger") self._hide_popup() self.request_completion() return # trigger length if (not self._completer.popup().isVisible() and event.text().isalnum()): prefix_len = len(self.completion_prefix) if prefix_len >= self._trigger_len: _logger().debug("cc: Len trigger") self.request_completion() return if self.completion_prefix == "": return self._hide_popup() def _on_selected_completion_changed(self, completion): self._current_completion = completion def _on_cursor_position_changed(self): current_line = TextHelper(self.editor).current_line_nbr() if current_line != self._cursor_line: self._cursor_line = current_line self._hide_popup() self._job_runner.cancel_requests() @QtCore.Slot() def _set_wait_cursor(self): # self.editor.set_mouse_cursor(QtCore.Qt.WaitCursor) pass def _is_last_char_end_of_word(self): try: cursor = self._helper.word_under_cursor() cursor.setPosition(cursor.position()) cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor) line = cursor.selectedText() last_char = line[len(line) - 1] if last_char != ' ': symbols = self._trigger_symbols seps = self.editor.word_separators return last_char in seps and last_char not in symbols return False except (IndexError, TypeError): return False def _show_completions(self, completions): _logger().info("showing %d completions" % len(completions)) self._job_runner.cancel_requests() # user typed too fast: end of word char has been inserted if (not self._cancel_next and not self._is_last_char_end_of_word() and not (len(completions) == 1 and completions[0]['name'] == self.completion_prefix)): # we can show the completer t = time.time() self._update_model(completions) elapsed = time.time() - t _logger().info("completion model updated: %d items in %f seconds", self._completer.model().rowCount(), elapsed) self._show_popup() _logger().info("popup shown") self._cancel_next = False def _handle_completer_events(self, event): nav_key = self._is_navigation_key(event) mod = QtCore.Qt.ControlModifier ctrl = int(event.modifiers() & mod) == mod # complete if (event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return or event.key() == QtCore.Qt.Key_Tab): self._insert_completion(self._current_completion) self._hide_popup() event.accept() # hide elif (event.key() == QtCore.Qt.Key_Escape or event.key() == QtCore.Qt.Key_Backtab or nav_key and ctrl): self._hide_popup() # move into list elif event.key() == QtCore.Qt.Key_Home: self._show_popup(index=0) event.accept() elif event.key() == QtCore.Qt.Key_End: self._show_popup(index=self._completer.completionCount() - 1) event.accept() def _hide_popup(self): if (self._completer.popup() is not None and self._completer.popup().isVisible()): self._completer.popup().hide() QtWidgets.QToolTip.hideText() self._job_runner.cancel_requests() def _show_popup(self, index=0): full_prefix = self._helper.word_under_cursor( select_whole_word=False).selectedText() if self._case_sensitive: self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive) else: self._completer.setCaseSensitivity( QtCore.Qt.CaseInsensitive) # set prefix self._completer.setCompletionPrefix(self.completion_prefix) cnt = self._completer.completionCount() selected = self._completer.currentCompletion() if (full_prefix == selected) and cnt == 1: self._hide_popup() else: # compute size and pos cursor_rec = self.editor.cursorRect() char_width = self.editor.fontMetrics().width('A') prefix_len = (len(self.completion_prefix) * char_width) cursor_rec.translate(self.editor.panels.margin_size() - prefix_len, # top self.editor.panels.margin_size(0) + 5) popup = self._completer.popup() width = popup.verticalScrollBar().sizeHint().width() cursor_rec.setWidth(self._completer.popup().sizeHintForColumn(0) + width) # show the completion list if self.editor.isVisible(): if not self._completer.popup().isVisible(): self._on_focus_in(None) self._completer.complete(cursor_rec) self._completer.popup().setCurrentIndex( self._completer.completionModel().index(index, 0)) else: _logger().debug('cannot show popup, editor is unvisible') def _insert_completion(self, completion): cursor = self._helper.word_under_cursor(select_whole_word=False) cursor.insertText(completion) self.editor.setTextCursor(cursor) def _is_shortcut(self, event): """ Checks if the event's key and modifiers make the completion shortcut (Ctrl+Space) :param event: QKeyEvent :return: bool """ modifier = (QtCore.Qt.MetaModifier if sys.platform == 'darwin' else QtCore.Qt.ControlModifier) valid_modifier = int(event.modifiers() & modifier) == modifier valid_key = event.key() == self._trigger_key _logger().debug("CC: Valid Mofifier: %r, Valid Key: %r" % (valid_modifier, valid_key)) return valid_key and valid_modifier @staticmethod def strip_control_characters(input_txt): """ Strips control character from ``input_txt`` :param input_txt: text to strip. :return: stripped text """ if input_txt: # unicode invalid characters re_illegal = \ '([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \ '|' + \ '([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \ (chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff), chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff), chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff)) input_txt = re.sub(re_illegal, "", input_txt) # ascii control characters input_txt = re.sub(r"[\x01-\x1F\x7F]", "", input_txt) return input_txt def _is_printable_key_event(self, event): return (event.text().isalnum() or event.text() in self.editor.word_separators) @staticmethod @memoized def _make_icon(icon): return QtGui.QIcon(icon) def _update_model(self, completions): """ Creates a QStandardModel that holds the suggestion from the completion models for the QCompleter :param completionPrefix: """ # build the completion model cc_model = QtGui.QStandardItemModel() self._tooltips.clear() for completion in completions: name = completion['name'] # skip redundant completion # if (name and name != self.completion_prefix and # name not in displayed_texts): item = QtGui.QStandardItem() item.setData(name, QtCore.Qt.DisplayRole) if 'tooltip' in completion and completion['tooltip']: self._tooltips[name] = completion['tooltip'] if 'icon' in completion: item.setData(self._make_icon(completion['icon']), QtCore.Qt.DecorationRole) cc_model.appendRow(item) self._completer.setModel(cc_model) return cc_model def _display_completion_tooltip(self, completion): if not self._show_tooltips: return if completion not in self._tooltips: QtWidgets.QToolTip.hideText() return tooltip = self._tooltips[completion].strip() pos = self._completer.popup().pos() pos.setX(pos.x() + self._completer.popup().size().width()) pos.setY(pos.y() - 15) QtWidgets.QToolTip.showText(pos, tooltip, self.editor) def _collect_completions(self, code, line, column, path, encoding, completion_prefix): _logger().debug("cc: completion requested") data = {'code': code, 'line': line, 'column': column, 'path': path, 'encoding': encoding, 'prefix': completion_prefix} try: self.editor.backend.send_request( backend.CodeCompletionWorker, args=data, on_receive=self._on_results_available) except NotRunning: self._data = data QtCore.QTimer.singleShot(100, self._retry_collect) _logger().debug('retry collect_completions in 100ms') else: self._set_wait_cursor() def _retry_collect(self): _logger().debug('retry work request') try: self.editor.backend.send_request( backend.CodeCompletionWorker, args=self._data, on_receive=self._on_results_available) except NotRunning: QtCore.QTimer.singleShot(100, self._retry_collect) else: self._set_wait_cursor() def clone_settings(self, original): self.trigger_key = original.trigger_key self.trigger_length = original.trigger_length self.trigger_symbols = original.trigger_symbols self.show_tooltips = original.show_tooltips self.case_sensitive = original.case_sensitive
def test_select_whole_line(editor): cursor = TextHelper(editor).select_whole_line(2) assert isinstance(cursor, QtGui.QTextCursor) assert cursor.hasSelection() assert cursor.selectionStart() == 28 assert cursor.selectionEnd() == 99
def on_install(self, editor): super(SearchAndReplacePanel, self).on_install(editor) self.hide() self.text_helper = TextHelper(editor)
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
def on_install(self, editor): self._create_completer() self._completer.setModel(QtGui.QStandardItemModel()) self._helper = TextHelper(editor) Mode.on_install(self, editor)
class CodeCompletionMode(Mode, QtCore.QObject): """ Provides code completions when typing or when pressing Ctrl+Space. This mode provides a code completion system which is extensible. It takes care of running the completion request in a background process using one or more completion provider and display the results in a QCompleter. To add code completion for a specific language, you only need to implement a new :class:`pyqode.core.backend.workers.CodeCompletionWorker.Provider` The completion popup is shown when the user press **ctrl+space** or automatically while the user is typing some code (this can be configured using a series of properties). """ #: Filter completions based on the prefix. FAST FILTER_PREFIX = 0 #: Filter completions based on whether the prefix is contained in the #: suggestion. Only available with PyQt5, if set with PyQt4, FILTER_PREFIX #: will be used instead. FAST FILTER_CONTAINS = 1 #: Fuzzy filtering, using the subsequence matcher. This is the most #: powerful filter mode but also the SLOWEST. FILTER_FUZZY = 2 @property def filter_mode(self): """ The completion filter mode """ return self._filter_mode @filter_mode.setter def filter_mode(self, value): self._filter_mode = value self._create_completer() @property def trigger_key(self): """ The key that triggers code completion (Default is **Space**: Ctrl + Space). """ return self._trigger_key @trigger_key.setter def trigger_key(self, value): self._trigger_key = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).trigger_key = value except KeyError: # this should never happen since we're working with clones pass @property def trigger_length(self): """ The trigger length defines the word length required to run code completion. """ return self._trigger_len @trigger_length.setter def trigger_length(self, value): self._trigger_len = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).trigger_length = value except KeyError: # this should never happen since we're working with clones pass @property def trigger_symbols(self): """ Defines the list of symbols that immediately trigger a code completion requiest. BY default, this list contains the dot character. For C++, we would add the '->' operator to that list. """ return self._trigger_symbols @trigger_symbols.setter def trigger_symbols(self, value): self._trigger_symbols = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).trigger_symbols = value except KeyError: # this should never happen since we're working with clones pass @property def case_sensitive(self): """ True to performs case sensitive completion matching. """ return self._case_sensitive @case_sensitive.setter def case_sensitive(self, value): self._case_sensitive = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).case_sensitive = value except KeyError: # this should never happen since we're working with clones pass @property def completion_prefix(self): """ Returns the current completion prefix """ return self._helper.word_under_cursor( select_whole_word=False).selectedText().strip() @property def show_tooltips(self): """ True to show tooltips next to the current completion. """ return self._show_tooltips @show_tooltips.setter def show_tooltips(self, value): self._show_tooltips = value if self.editor: # propagate changes to every clone for clone in self.editor.clones: try: clone.modes.get(CodeCompletionMode).show_tooltips = value except KeyError: # this should never happen since we're working with clones pass def __init__(self): Mode.__init__(self) QtCore.QObject.__init__(self) self._current_completion = "" self._trigger_key = QtCore.Qt.Key_Space self._trigger_len = 1 self._trigger_symbols = ['.'] self._case_sensitive = False self._completer = None self._filter_mode = self.FILTER_FUZZY self._last_cursor_line = -1 self._last_cursor_column = -1 self._tooltips = {} self._show_tooltips = False self._request_id = self._last_request_id = 0 def clone_settings(self, original): self.trigger_key = original.trigger_key self.trigger_length = original.trigger_length self.trigger_symbols = original.trigger_symbols self.show_tooltips = original.show_tooltips self.case_sensitive = original.case_sensitive # # Mode interface # def _create_completer(self): if self.filter_mode != self.FILTER_FUZZY: self._completer = QtWidgets.QCompleter([''], self.editor) if self.filter_mode == self.FILTER_CONTAINS: try: self._completer.setFilterMode(QtCore.Qt.MatchContains) except AttributeError: # only available with PyQt5 pass else: self._completer = SubsequenceCompleter(self.editor) self._completer.setCompletionMode(self._completer.PopupCompletion) if self.case_sensitive: self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive) else: self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) self._completer.activated.connect(self._insert_completion) self._completer.highlighted.connect( self._on_selected_completion_changed) self._completer.highlighted.connect(self._display_completion_tooltip) def on_install(self, editor): self._create_completer() self._completer.setModel(QtGui.QStandardItemModel()) self._helper = TextHelper(editor) Mode.on_install(self, editor) def on_uninstall(self): Mode.on_uninstall(self) self._completer.popup().hide() self._completer = None def on_state_changed(self, state): if state: self.editor.focused_in.connect(self._on_focus_in) self.editor.key_pressed.connect(self._on_key_pressed) self.editor.post_key_pressed.connect(self._on_key_released) else: self.editor.focused_in.disconnect(self._on_focus_in) self.editor.key_pressed.disconnect(self._on_key_pressed) self.editor.post_key_pressed.disconnect(self._on_key_released) # # Slots # def _on_key_pressed(self, event): def _handle_completer_events(): nav_key = self._is_navigation_key(event) mod = QtCore.Qt.ControlModifier ctrl = int(event.modifiers() & mod) == mod # complete if event.key() in [ QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return, QtCore.Qt.Key_Tab]: self._insert_completion(self._current_completion) self._hide_popup() event.accept() # hide elif (event.key() in [ QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backtab] or nav_key and ctrl): self._reset_sync_data() # move into list elif event.key() == QtCore.Qt.Key_Home: self._show_popup(index=0) event.accept() elif event.key() == QtCore.Qt.Key_End: self._show_popup(index=self._completer.completionCount() - 1) event.accept() debug('key pressed: %s' % event.text()) is_shortcut = self._is_shortcut(event) # handle completer popup events ourselves if self._completer.popup().isVisible(): if is_shortcut: event.accept() else: _handle_completer_events() elif is_shortcut: self._reset_sync_data() self.request_completion() event.accept() def _on_key_released(self, event): if self._is_shortcut(event) or event.isAccepted(): return debug('key released:%s' % event.text()) word = self._helper.word_under_cursor( select_whole_word=True).selectedText() debug('word: %s' % word) if event.text(): if event.key() == QtCore.Qt.Key_Escape: self._hide_popup() return if self._is_navigation_key(event) and \ (not self._is_popup_visible() or word == ''): self._reset_sync_data() return if event.key() == QtCore.Qt.Key_Return: return if event.text() in self._trigger_symbols: # symbol trigger, force request self._reset_sync_data() self.request_completion() elif len(word) >= self._trigger_len and event.text() not in \ self.editor.word_separators: # Length trigger if int(event.modifiers()) in [ QtCore.Qt.NoModifier, QtCore.Qt.ShiftModifier]: self.request_completion() else: self._hide_popup() else: self._reset_sync_data() else: if self._is_navigation_key(event): if self._is_popup_visible() and word: self._show_popup() return else: self._reset_sync_data() def _on_focus_in(self, event): """ Resets completer's widget :param event: QFocusEvents """ self._completer.setWidget(self.editor) def _on_selected_completion_changed(self, completion): self._current_completion = completion def _insert_completion(self, completion): cursor = self._helper.word_under_cursor(select_whole_word=False) cursor.insertText(completion) self.editor.setTextCursor(cursor) def _on_results_available(self, results): debug("completion results (completions=%r), prefix=%s", results, self.completion_prefix) context = results[0] results = results[1:] line, column, request_id = context debug('request context: %r', context) debug('latest context: %r', (self._last_cursor_line, self._last_cursor_column, self._request_id)) self._last_request_id = request_id if (line == self._last_cursor_line and column == self._last_cursor_column): if self.editor: all_results = [] for res in results: all_results += res self._show_completions(all_results) else: debug('outdated request, dropping') # # Helper methods # def _is_popup_visible(self): return self._completer.popup().isVisible() def _reset_sync_data(self): debug('reset sync data and hide popup') self._last_cursor_line = -1 self._last_cursor_column = -1 self._hide_popup() def request_completion(self): line = self._helper.current_line_nbr() column = self._helper.current_column_nbr() - \ len(self.completion_prefix) same_context = (line == self._last_cursor_line and column == self._last_cursor_column) if same_context: if self._request_id - 1 == self._last_request_id: # context has not changed and the correct results can be # directly shown debug('request completion ignored, context has not ' 'changed') self._show_popup() else: # same context but result not yet available pass return True else: debug('requesting completion') data = { 'code': self.editor.toPlainText(), 'line': line, 'column': column, 'path': self.editor.file.path, 'encoding': self.editor.file.encoding, 'prefix': self.completion_prefix, 'request_id': self._request_id } try: self.editor.backend.send_request( backend.CodeCompletionWorker, args=data, on_receive=self._on_results_available) except NotRunning: _logger().exception('failed to send the completion request') return False else: debug('request sent: %r', data) self._last_cursor_column = column self._last_cursor_line = line self._request_id += 1 return True def _is_shortcut(self, event): """ Checks if the event's key and modifiers make the completion shortcut (Ctrl+Space) :param event: QKeyEvent :return: bool """ modifier = (QtCore.Qt.MetaModifier if sys.platform == 'darwin' else QtCore.Qt.ControlModifier) valid_modifier = int(event.modifiers() & modifier) == modifier valid_key = event.key() == self._trigger_key return valid_key and valid_modifier def _hide_popup(self): """ Hides the completer popup """ debug('hide popup') if (self._completer.popup() is not None and self._completer.popup().isVisible()): self._completer.popup().hide() self._last_cursor_column = -1 self._last_cursor_line = -1 QtWidgets.QToolTip.hideText() def _get_popup_rect(self): cursor_rec = self.editor.cursorRect() char_width = self.editor.fontMetrics().width('A') prefix_len = (len(self.completion_prefix) * char_width) cursor_rec.translate( self.editor.panels.margin_size() - prefix_len, self.editor.panels.margin_size(0) + 5) popup = self._completer.popup() width = popup.verticalScrollBar().sizeHint().width() cursor_rec.setWidth( self._completer.popup().sizeHintForColumn(0) + width) return cursor_rec def _show_popup(self, index=0): """ Shows the popup at the specified index. :param index: index :return: """ full_prefix = self._helper.word_under_cursor( select_whole_word=False).selectedText() if self._case_sensitive: self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive) else: self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) # set prefix self._completer.setCompletionPrefix(self.completion_prefix) cnt = self._completer.completionCount() selected = self._completer.currentCompletion() if (full_prefix == selected) and cnt == 1: debug('user already typed the only completion that we ' 'have') self._hide_popup() else: # show the completion list if self.editor.isVisible(): if self._completer.widget() != self.editor: self._completer.setWidget(self.editor) self._completer.complete(self._get_popup_rect()) self._completer.popup().setCurrentIndex( self._completer.completionModel().index(index, 0)) debug( "popup shown: %r" % self._completer.popup().isVisible()) else: debug('cannot show popup, editor is not visible') def _show_completions(self, completions): debug("showing %d completions" % len(completions)) debug('popup state: %r', self._completer.popup().isVisible()) t = time.time() self._update_model(completions) elapsed = time.time() - t debug("completion model updated: %d items in %f seconds", self._completer.model().rowCount(), elapsed) self._show_popup() def _update_model(self, completions): """ Creates a QStandardModel that holds the suggestion from the completion models for the QCompleter :param completionPrefix: """ # build the completion model cc_model = QtGui.QStandardItemModel() self._tooltips.clear() for completion in completions: name = completion['name'] item = QtGui.QStandardItem() item.setData(name, QtCore.Qt.DisplayRole) if 'tooltip' in completion and completion['tooltip']: self._tooltips[name] = completion['tooltip'] if 'icon' in completion: icon = completion['icon'] if isinstance(icon, list): icon = QtGui.QIcon.fromTheme(icon[0], QtGui.QIcon(icon[1])) else: icon = QtGui.QIcon(icon) item.setData(QtGui.QIcon(icon), QtCore.Qt.DecorationRole) cc_model.appendRow(item) try: self._completer.setModel(cc_model) except RuntimeError: self._create_completer() self._completer.setModel(cc_model) return cc_model def _display_completion_tooltip(self, completion): if not self._show_tooltips: return if completion not in self._tooltips: QtWidgets.QToolTip.hideText() return tooltip = self._tooltips[completion].strip() pos = self._completer.popup().pos() pos.setX(pos.x() + self._completer.popup().size().width()) pos.setY(pos.y() - 15) QtWidgets.QToolTip.showText(pos, tooltip, self.editor) @staticmethod def _is_navigation_key(event): return (event.key() == QtCore.Qt.Key_Backspace or event.key() == QtCore.Qt.Key_Back or event.key() == QtCore.Qt.Key_Delete or event.key() == QtCore.Qt.Key_End or event.key() == QtCore.Qt.Key_Home or event.key() == QtCore.Qt.Key_Left or event.key() == QtCore.Qt.Key_Right or event.key() == QtCore.Qt.Key_Up or event.key() == QtCore.Qt.Key_Down or event.key() == QtCore.Qt.Key_Space)