def _on_item_activated(item, *args): assert isinstance(item, QtWidgets.QTreeWidgetItem) data = item.data(0, QtCore.Qt.UserRole) try: l = data['line'] except TypeError: return # file item or root item start = data['start'] lenght = data['end'] - start if data is not None: # open editor and go to line/column e = editor.open_file(data['path'], data['line'], data['start']) if e is None: return # select text helper = TextHelper(e) try: cursor = helper.select_lines(start=l, end=l) except AttributeError: _logger().debug('failed to select occurent line in editor, not' ' a subclass of QPlainTextEdit') else: assert isinstance(cursor, QtGui.QTextCursor) cursor.movePosition(cursor.StartOfBlock) cursor.movePosition(cursor.Right, cursor.MoveAnchor, start) cursor.movePosition(cursor.Right, cursor.KeepAnchor, lenght) e.setTextCursor(cursor)
def _on_mouse_released(self, event): """ mouse pressed callback """ if event.button() == 1 and self._deco: cursor = TextHelper(self.editor).word_under_mouse_cursor() if cursor and cursor.selectedText(): self._timer.request_job( self.word_clicked.emit, cursor)
def _check_word_cursor(self, tc=None): """ Request a go to assignment. :param tc: Text cursor which contains the text that we must look for its assignment. Can be None to go to the text that is under the text cursor. :type tc: QtGui.QTextCursor """ if not tc: tc = TextHelper(self.editor).word_under_cursor() request_data = { 'code': self.editor.toPlainText(), 'line': tc.blockNumber(), 'column': tc.columnNumber(), 'path': self.editor.file.path, 'encoding': self.editor.file.encoding } try: self.editor.backend.send_request( workers.goto_assignments, request_data, on_receive=self._on_results_available) except NotRunning: pass
def _select_word_under_mouse_cursor(self): """ Selects the word under the mouse cursor. """ cursor = TextHelper(self.editor).word_under_mouse_cursor() if (self._previous_cursor_start != cursor.selectionStart() and self._previous_cursor_end != cursor.selectionEnd()): self._remove_decoration() self._add_decoration(cursor) self._previous_cursor_start = cursor.selectionStart() self._previous_cursor_end = cursor.selectionEnd()
def _on_mouse_moved(self, event): """ mouse moved callback """ if event.modifiers() & QtCore.Qt.ControlModifier: cursor = TextHelper(self.editor).word_under_mouse_cursor() if not self._cursor or cursor.position() != self._cursor.position(): self._check_word_cursor(cursor) self._cursor = cursor else: self._cursor = None self._clear_selection()
def _on_key_pressed(self, event): helper = TextHelper(self.editor) indent = helper.line_indent() * ' ' if self.editor.textCursor().positionInBlock() == len(indent): self.QUOTES_FORMATS['"'] = '%s:' else: self.QUOTES_FORMATS['"'] = '%s' self.QUOTES_FORMATS['{'] = '\n' + indent + '%s' self.QUOTES_FORMATS['['] = '\n' + indent + '%s' super(AutoCompleteMode, self)._on_key_pressed(event)
def _on_action_quick_doc_triggered(self): tc = TextHelper(self.editor).word_under_cursor(select_whole_word=True) request_data = { 'code': self.editor.toPlainText(), 'line': tc.blockNumber(), 'column': tc.columnNumber(), 'path': self.editor.file.path, 'encoding': self.editor.file.encoding } self.editor.backend.send_request( quick_doc, request_data, on_receive=self._on_results_available)
def _in_method_call(self): helper = TextHelper(self.editor) line_nbr = helper.current_line_nbr() - 1 expected_indent = helper.line_indent() - 4 while line_nbr >= 0: text = helper.line_text(line_nbr) indent = len(text) - len(text.lstrip()) if indent == expected_indent and 'class' in text: return True line_nbr -= 1 return False
def test_dynamic_folding_insert_section(): # test insert section under empty data division # data division (which is not a fold trigger initially) block will become a fold trigger editor = CobolCodeEdit() editor.setPlainText(section_code, '', '') th = TextHelper(editor) block = editor.document().findBlockByNumber(2) assert TextBlockHelper.is_fold_trigger(block) is False cursor = th.goto_line(2, column=len(' DATA DIVISION.')) cursor.insertText('\n WORKING-STORAGE SECTION.') assert TextBlockHelper.is_fold_trigger(block) is True
def test_add_decoration(editor): helper = TextHelper(editor) helper.goto_line(2, 2) cursor = helper.word_under_cursor(select_whole_word=True) deco = TextDecoration(cursor) deco.set_as_bold() deco.set_as_underlined(QtGui.QColor("#FF0000")) editor.decorations.append(deco) assert not editor.decorations.append(deco) assert deco.contains_cursor(cursor) # keep editor clean for next tests editor.decorations.clear()
def _on_post_key_pressed(self, event): if not event.isAccepted() and not self._ignore_post: txt = event.text() next_char = TextHelper(self.editor).get_right_character() if txt in self.MAPPING: to_insert = self.MAPPING[txt] if (not next_char or next_char in self.MAPPING.keys() or next_char in self.MAPPING.values() or next_char.isspace()): TextHelper(self.editor).insert_text( self.QUOTES_FORMATS[txt] % to_insert) self._ignore_post = False
def _on_item_clicked(self, item): """ Go to the item position in the editor. """ if item: name = item.data(0, QtCore.Qt.UserRole) if name: go = name.block.blockNumber() helper = TextHelper(self._editor) if helper.current_line_nbr() != go: helper.goto_line(go, column=name.column) self._editor.setFocus()
def test_clear_decoration(editor): # should work even when there are no more decorations helper = TextHelper(editor) editor.decorations.clear() cursor = helper.word_under_cursor(select_whole_word=True) deco = TextDecoration(cursor) deco.set_as_bold() deco.set_as_underlined(QtGui.QColor("#FF0000")) editor.decorations.append(deco) assert not editor.decorations.append(deco) editor.decorations.clear() assert editor.decorations.append(deco) assert editor.decorations.remove(deco)
def test_remove_decoration(editor): helper = TextHelper(editor) TextHelper(editor).goto_line(1, 2) cursor = helper.word_under_cursor(select_whole_word=True) deco = TextDecoration(cursor) deco.set_as_bold() deco.set_as_underlined(QtGui.QColor("#FF0000")) editor.decorations.append(deco) assert editor.decorations.remove(deco) # already removed, return False assert not editor.decorations.remove(deco) assert editor.decorations.append(deco) # keep editor clean for next tests editor.decorations.clear()
def _on_bt_toggled(self, *ar): if not self._lock: self._lock = True for w in self._widgets: if w == self.sender(): break if self._toggled: self._toggled.setChecked(False) self._toggled = w th = TextHelper(self.editor) line = w.scope.blockNumber() th.goto_line(line, column=th.line_indent(line)) self._lock = False self.editor.setFocus()
def _compute_offsets(self): original_tc = self.editor.textCursor() tc = self.editor.textCursor() start = tc.selectionStart() end = tc.selectionEnd() tc.setPosition(start) start_line = tc.blockNumber() tc.setPosition(end) end_line = tc.blockNumber() th = TextHelper(self.editor) th.select_lines(start=start_line, end=end_line, apply_selection=True) source = th.selected_text() results = get_field_infos(source, self.editor.free_format) self.editor.setTextCursor(original_tc) self.pic_infos_available.emit(results)
def _computeOffsets(self): original_tc = self.editor.textCursor() tc = self.editor.textCursor() assert isinstance(tc, QTextCursor) start = tc.selectionStart() end = tc.selectionEnd() tc.setPosition(start) start_line = tc.blockNumber() + 1 tc.setPosition(end) end_line = tc.blockNumber() + 1 th = TextHelper(self.editor) th.select_lines(start=start_line, end=end_line, apply_selection=True) source = th.selected_text() self.picInfosAvailable.emit(get_field_infos(source)) self.editor.setTextCursor(original_tc)
def test_dynamic_folding_insert_end_if(): # test insert section under empty data division # data division (which is not a fold trigger initially) block will become a fold trigger editor = CobolCodeEdit() editor.setPlainText(end_if_code, '', '') th = TextHelper(editor) block = editor.document().findBlockByNumber(4) first, last = FoldScope(block).get_range() assert first == 4 assert last == 5 cursor = th.goto_line(5, column=len(' DISPLAY "FOO"')) cursor.insertText('\n END-IF') first, last = FoldScope(block).get_range() assert first == 4 assert last == 6 editor.close()
def _on_post_key_pressed(self, event): if not event.isAccepted() and not self._ignore_post: txt = event.text() trav = self.editor.textCursor() assert isinstance(trav, QtGui.QTextCursor) trav.movePosition(trav.Left, trav.MoveAnchor, 2) literal = TextHelper(self.editor).is_comment_or_string(trav) if not literal: next_char = TextHelper(self.editor).get_right_character() if txt in self.MAPPING: to_insert = self.MAPPING[txt] if (not next_char or next_char in self.MAPPING.keys() or next_char in self.MAPPING.values() or next_char.isspace()): TextHelper(self.editor).insert_text( self.QUOTES_FORMATS[txt] % to_insert) self._ignore_post = False
def test_occurrences(editor, underlined): editor.file.open(__file__) mode = get_mode(editor) mode.underlined = underlined assert len(mode._decorations) == 0 assert mode.delay == 1000 TextHelper(editor).goto_line(16, 7) QTest.qWait(2000) assert len(mode._decorations) == 22
def _goto_symbol(self): try: self._cached_cursor_position = TextHelper( api.editor.get_current_editor()).cursor_position() except (TypeError, AttributeError): pass # no current editor or not a code edit else: self._locator.mode = self._locator.MODE_GOTO_SYMBOL self._show_locator()
def test_action_search_triggered2(editor): panel = get_panel(editor) # second search with the same text tc = TextHelper(editor).word_under_mouse_cursor() editor.setTextCursor(tc) panel.on_search() editor.show() QTest.qWait(1000) assert panel.isVisible()
def perform_word_selection(self, event=None): """ Performs word selection :param event: QMouseEvent """ self.editor.setTextCursor( TextHelper(self.editor).word_under_cursor(True)) if event: event.accept()
def test_request_completion(editor): mode = get_mode(editor) QTest.qWait(1000) if editor.backend.running: editor.backend.stop() # starts the server after the request to test the retry on NotConnected # mechanism TextHelper(editor).goto_line(0) QTest.qWait(2000) mode.request_completion() editor.backend.start(server_path()) QTest.qWait(1000) assert editor.backend.running is True # now this should work TextHelper(editor).goto_line(3) QTest.qWait(100) assert mode.request_completion() is True QTest.qWait(100)
def _goto_error_msg(self, msg): """ Opens an editor and goes to the error line. """ if os.path.exists(msg.path): self.app.file.open_file(msg.path) if msg.status: TextHelper(self.app.edit.current_editor).goto_line(msg.line) self.app.edit.current_editor.setFocus(True)
def perform_extended_selection(self, event=None): """ Performs extended word selection. :param event: QMouseEvent """ TextHelper(self.editor).select_extended_word( continuation_chars=self.continuation_characters) if event: event.accept()
def test_successive_requests(editor): mode = get_mode(editor) QTest.qWait(1000) TextHelper(editor).goto_line(3) # only the first request should be accepted ret1 = mode.request_completion() ret2 = mode.request_completion() assert ret1 is True assert ret2 is True
def on_goto_out_of_doc(self, assignment): """ Open the a new tab when goto goes out of the current document. :param assignment: Destination """ editor = self.open_file(assignment.module_path) if editor: TextHelper(editor).goto_line(assignment.line, assignment.column)
def test_line_selection(editor): QTest.qWait(1000) mode = get_mode(editor) TextHelper(editor).goto_line(0, 2) QTest.qWait(1000) mode.perform_line_selection() assert editor.textCursor().selectedText( ) == 'from pyqode.qt import QtCore, QtGui' QTest.qWait(1000)
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()) if line: 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].line) if top: self._job_runner.request_job(self._display_tooltip, text, top) else: self._job_runner.cancel_requests() self._previous_line = line
def requestGoTo(self, tc=None): """ Request a go to assignment. :param tc: Text cursor which contains the text that we must look for its assignment. Can be None to go to the text that is under the text cursor. :type tc: QtGui.QTextCursor """ if not tc: tc = TextHelper(self.editor).word_under_cursor( select_whole_word=True) symbol = tc.selectedText() analyser = getattr(self.editor, "analyserMode") if analyser: node = analyser.root_node.find(symbol) if node: self._definition = node QTimer.singleShot(100, self._goToDefinition)
def test_calltips(editor): editor.clear() mode = get_mode(editor) assert editor.backend.connected assert mode is not None editor.setPlainText("open(__file_") TextHelper(editor).goto_line(1, len('open(__file_')) QTest.keyPress(editor, ',') QTest.keyRelease(editor, ',') QTest.qWait(1000)
def _is_last_chard_end_of_word(self): try: tc = TextHelper(self.editor).word_under_cursor() tc.setPosition(tc.position()) tc.movePosition(tc.StartOfLine, tc.KeepAnchor) l = tc.selectedText() last_char = l[len(l) - 1] seps = self.editor.word_separators symbols = [",", " ", "("] return last_char in seps and last_char not in symbols except IndexError: return False
def _on_key_pressed(self, event): txt = event.text() cursor = self.editor.textCursor() from pyqode.qt import QtGui assert isinstance(cursor, QtGui.QTextCursor) if cursor.hasSelection(): # quoting of selected text if event.text() in self.MAPPING.keys(): first = event.text() last = self.MAPPING[event.text()] cursor.insertText(self.SELECTED_QUOTES_FORMATS[event.text()] % (first, cursor.selectedText(), last)) self.editor.setTextCursor(cursor) event.accept() else: self._ignore_post = True return next_char = TextHelper(self.editor).get_right_character() self.logger.debug('next char: %s', next_char) ignore = False if event.key() == QtCore.Qt.Key_Backspace: # get the character that will get deleted tc = self.editor.textCursor() pos = tc.position() tc.movePosition(tc.Left) tc.movePosition(tc.Right, tc.KeepAnchor) del_char = tc.selectedText() if del_char in self.MAPPING and \ self.MAPPING[del_char] == next_char: tc.beginEditBlock() tc.movePosition(tc.Right, tc.KeepAnchor) tc.insertText('') tc.setPosition(pos - 2) tc.endEditBlock() self.editor.setTextCursor(tc) ignore = True elif (txt and next_char == txt and (next_char in self.MAPPING or txt in self.AVOID_DUPLICATES)): ignore = True if ignore: event.accept() TextHelper(self.editor).clear_selection() TextHelper(self.editor).move_right()
def _get_indent(self, cursor): """ Return the indentation text (a series of spaces or tabs) :param cursor: QTextCursor :returns: Tuple (text before new line, text after new line) """ indent = TextHelper(self.editor).line_indent() * ' ' return "", indent
def _get_indent(self, cursor): prev_line_text = TextHelper(self.editor).current_line_text() if not self.editor.free_format: prev_line_text = " " * 7 + prev_line_text[7:] diff = len(prev_line_text) - len(prev_line_text.lstrip()) post_indent = " " * diff min_column = self.editor.indenter_mode.min_column if len(post_indent) < min_column: post_indent = min_column * " " # all regex are upper cases text = cursor.block().text().upper() # raise indentation level patterns = [regex.PARAGRAPH_PATTERN, regex.STRUCT_PATTERN, regex.BRANCH_START, regex.LOOP_PATTERN] for ptrn in patterns: if ptrn.indexIn(text) != -1: post_indent += self.editor.tab_length * " " return "", post_indent # use the previous line indentation return "", post_indent
def inner_action(*args): """ Inner action: open file """ # cache cursor position before reloading so that the cursor position # is restored automatically after reload has finished. # See OpenCobolIDE/OpenCobolIDE#97 Cache().set_cursor_position( self.editor.file.path, TextHelper(self.editor).cursor_position()) self.editor.file.open(self.editor.file.path) self.file_reloaded.emit()
def _on_replace_triggered(self): text = '' if editor.get_current_editor() is not None: text = TextHelper(editor.get_current_editor()).selected_text() search_settings = _DlgFindReplace.replace(self.main_window, text_to_find=text) if search_settings is not None: self._remove_dock() self._replace = True self._start_search_in_path(search_settings)
def open_editor(path, line): global editor_windows print(path, line) editor = PyCodeEdit() # prevent the restoration of cursor position which will reset the position # we will set after opening the file editor.file.restore_cursor = False editor.file.open(path) TextHelper(editor).goto_line(line) editor.show() editor_windows.append(editor)
def _update_status_bar(self, editor): if editor: l, c = TextHelper(editor).cursor_position() self.lbl_cursor_pos.setText('%d:%d' % (l + 1, c + 1)) self.lbl_encoding.setText(editor.file.encoding) self.lbl_filename.setText(editor.file.path) self.lbl_interpreter.setText(sys.executable) else: self.lbl_encoding.clear() self.lbl_filename.clear() self.lbl_cursor_pos.clear()
def test_forbidden_characters(editor): code = '''{ "widget": { "debug": "on", "window": { "title": "Sample Konfabulator Widget", "name": "main_window", "width": "height": 500 }, } } ''' editor.setPlainText(code) assert editor.toPlainText() == code TextHelper(editor).goto_line(6, column=16, move=True) QTest.qWait(100) assert TextHelper(editor).current_line_text() == ' "width":' QTest.keyPress(editor, '\'') assert TextHelper(editor).current_line_text() == ' "width":\''
def test_occurrences(editor): for underlined in [True, False]: editor.file.open(__file__) assert editor.backend.running is True mode = get_mode(editor) mode.underlined = underlined assert len(mode._decorations) == 0 assert mode.delay == 1000 TextHelper(editor).goto_line(16, 7) QTest.qWait(2000) assert len(mode._decorations) > 0
def test_calltips_with_closing_paren(editor): editor.clear() mode = get_mode(editor) assert editor.backend.connected assert mode is not None editor.setPlainText("open") TextHelper(editor).goto_line(1, len('open')) QTest.keyPress(editor, '(') QTest.keyRelease(editor, '(') QTest.qWait(1000) mode._display_tooltip(None, 0)
def color(self, value): self._color = value self._pen = QtGui.QPen(self._color) TextHelper(self.editor).mark_whole_doc_dirty() self.editor.repaint() if self.editor: for clone in self.editor.clones: try: clone.modes.get(self.__class__).color = value except KeyError: # this should never happen since we're working with clones pass
def test_matching(editor): """ Moves the text cursor in a few places to execute all statements in matcher.py. """ mode = get_mode(editor) # before [ TextHelper(editor).goto_line(1, 14) mode.do_symbols_matching() # after ] TextHelper(editor).goto_line(4, 1) mode.do_symbols_matching() # before { TextHelper(editor).goto_line(5, 14) mode.do_symbols_matching() # after } TextHelper(editor).goto_line(8, 1) mode.do_symbols_matching() # before ( TextHelper(editor).goto_line(9, 16) mode.do_symbols_matching() # after ) TextHelper(editor).goto_line(12, 1) mode.do_symbols_matching()
def _on_find_triggered(self): text = '' if editor.get_current_editor() is not None: try: text = TextHelper(editor.get_current_editor()).selected_text() except AttributeError: text = '' search_settings = _DlgFindReplace.find(self.main_window, text_to_find=text) if search_settings is not None: self._remove_dock() self._replace = False self._start_search_in_path(search_settings)
def test_goto_out_of_doc(editor): global out out = False editor.clear() code = "import logging\nlogging.basicConfig()" editor.setPlainText(code) mode = get_mode(editor) TextHelper(editor).goto_line(1, len('logging.basicConfig()') - 4) mode.out_of_doc.connect(_on_out_of_doc) assert out is False mode.request_goto() QTest.qWait(5000) assert out is True
def test_symbol_pos(editor): """ Moves the text cursor in a few places to execute all statements in matcher.py. """ mode = get_mode(editor) editor.file.open(__file__) # move to ')' cursor = TextHelper(editor).goto_line(12, 1, move=False) l, c = mode.symbol_pos(cursor) assert l == 9 assert c == 17
def test_action_search_triggered(editor): panel = get_panel(editor) # select word under cursor tc = TextHelper(editor).word_under_mouse_cursor() editor.setTextCursor(tc) panel.on_actionSearch_triggered() assert panel.isVisible() QTest.qWait(1000) panel.checkBoxCase.setChecked(True) panel.checkBoxWholeWords.setChecked(True) panel.on_actionSearch_triggered() assert panel.isVisible() QTest.qWait(1000)
def test_multiple_results(editor): global flg_multi editor.clear() code = "import os\nos.path.abspath('..')" editor.setPlainText(code) mode = get_mode(editor) TextHelper(editor).goto_line(1, 4) QTest.qWait(5000) mode.request_goto() assert flg_multi is False QtCore.QTimer.singleShot(1000, accept_dlg) QTest.qWait(5000) assert flg_multi is True
def _get_indent(self, cursor): prev_line_text = TextHelper(self.editor).current_line_text() if not self.editor.free_format: prev_line_text = ' ' * 7 + prev_line_text[7:] diff = len(prev_line_text) - len(prev_line_text.lstrip()) post_indent = ' ' * diff min_column = self.editor.indenter_mode.min_column if len(post_indent) < min_column: post_indent = min_column * ' ' # all regex are upper cases text = cursor.block().text().upper() # raise indentation level patterns = [ regex.PARAGRAPH_PATTERN, regex.STRUCT_PATTERN, regex.BRANCH_START, regex.LOOP_PATTERN ] for ptrn in patterns: if ptrn.indexIn(text) != -1: post_indent += self.editor.tab_length * ' ' return '', post_indent # use the previous line indentation return '', post_indent
def _on_occurrence_activated(item): assert isinstance(item, QtWidgets.QTreeWidgetItem) data = item.data(0, QtCore.Qt.UserRole) try: l = data['line'] except TypeError: return # file item or root item l = data['line'] start = data['start'] lenght = data['end'] - start if data is not None: # open editor and go to line/column editor = api.editor.open_file( data['path'], data['line'], data['start']) if editor is None: return # select text helper = TextHelper(editor) cursor = helper.select_lines(start=l, end=l) cursor.movePosition(cursor.StartOfBlock) cursor.movePosition(cursor.Right, cursor.MoveAnchor, start) cursor.movePosition(cursor.Right, cursor.KeepAnchor, lenght) editor.setTextCursor(cursor)
def request_goto(self, tc=None): """ Request a go to assignment. :param tc: Text cursor which contains the text that we must look for its assignment. Can be None to go to the text that is under the text cursor. :type tc: QtGui.QTextCursor """ if not tc: tc = TextHelper(self.editor).word_under_cursor() if not self._pending: request_data = { 'code': self.editor.toPlainText(), 'line': tc.blockNumber() + 1, 'column': tc.columnNumber(), 'path': self.editor.file.path, 'encoding': self.editor.file.encoding } self.editor.backend.send_request( workers.goto_assignments, request_data, on_receive=self._on_results_available) self._pending = True self.editor.set_mouse_cursor(QtCore.Qt.WaitCursor)
def _on_post_key_pressed(self, event): # if we are in disabled cc, use the parent implementation helper = TextHelper(self.editor) column = helper.current_column_nbr() usd = self.editor.textCursor().block().userData() if usd: for start, end in usd.cc_disabled_zones: if (start <= column < end - 1 and not helper.current_line_text( ).lstrip().startswith('"""')): return prev_line = helper.line_text(helper.current_line_nbr() - 1) is_below_fct_or_class = "def" in prev_line or "class" in prev_line if (event.text() == '"' and '""' == helper.current_line_text().strip() and (is_below_fct_or_class or column == 2)): self._insert_docstring(prev_line, is_below_fct_or_class) elif (event.text() == "(" and helper.current_line_text().lstrip().startswith("def ")): self._handle_fct_def() else: super(PyAutoCompleteMode, self)._on_post_key_pressed(event)
def _auto_import(self): helper = TextHelper(self.editor) name = helper.word_under_cursor(select_whole_word=True).selectedText() if name: import_stmt = 'import %s' % name else: import_stmt = '' import_stmt, status = QtWidgets.QInputDialog.getText( self.editor, _('Add import'), _('Import statement:'), QtWidgets.QLineEdit.Normal, import_stmt) if status: sh = self.editor.syntax_highlighter line_number = sh.import_statements[0].blockNumber() for stmt in sh.import_statements: if stmt.text() == import_stmt: # same import already exists return l, c = helper.cursor_position() cursor = helper.goto_line(line_number) cursor.insertText(import_stmt + '\n') helper.goto_line(l + 1, c)
class PyAutoIndentMode(AutoIndentMode): """ Customised :class:`pyqode.core.modes.AutoIndentMode` for python that tries its best to follow the pep8 indentation guidelines. """ def __init__(self): super(PyAutoIndentMode, self).__init__() def on_install(self, editor): super().on_install(editor) self._helper = TextHelper(editor) def has_two_empty_line_before(self, tc): ln = tc.blockNumber() limit = ln - 1 while ln > limit: if self._helper.line_text(ln).strip() != "": return False ln -= 1 return True def has_unclosed_paren(self, tc): ln = tc.blockNumber() while ln >= 0: line = self._helper.line_text(ln) if line.count("(") > line.count(")"): return True ln -= 1 return False def is_in_string_def(self, full_line, column): count = 0 char = "'" for i in range(len(full_line)): if full_line[i] == "'" or full_line[i] == '"': count += 1 if full_line[i] == '"' and i < column: char = '"' count_after_col = 0 for i in range(column, len(full_line)): if full_line[i] == "'" or full_line[i] == '"': count_after_col += 1 return count % 2 == 0 and count_after_col % 2 == 1, char def is_paren_open(self, paren): return (paren.character == "(" or paren.character == "[" or paren.character == '{') def is_paren_closed(self, paren): return (paren.character == ")" or paren.character == "]" or paren.character == '}') def get_full_line(self, tc): tc2 = QTextCursor(tc) tc2.select(QTextCursor.LineUnderCursor) full_line = tc2.selectedText() return full_line def parens_count_for_block(self, col, block): data = block.userData() nb_open = 0 nb_closed = 0 lists = [data.parentheses, data.braces, data.square_brackets] for symbols in lists: for paren in symbols: if self.is_paren_open(paren): if not col: return -1, -1 nb_open += 1 if paren.position >= col and self.is_paren_closed(paren): nb_closed += 1 return nb_closed, nb_open def between_paren(self, tc, col): nb_closed, nb_open = self.parens_count_for_block(col, tc.block()) block = tc.block().next() while nb_open == nb_closed == 0 and block.isValid(): nb_closed, nb_open = self.parens_count_for_block(nb_open, block) block = block.next() # if not, is there an non closed paren on the next lines. parens = {'(': 0, '{': 0, '[': 0} matching = {')': '(', '}': '{', ']': '['} rparens = {')': 0, '}': 0, ']': 0} rmatching = {'(': ')', '{': '}', '[': ']'} if nb_open != nb_closed: # look down if nb_open > nb_closed: operation = self._next_block down = True else: operation = self._prev_block down = False block = tc.block() # block = operation(tc.block()) offset = col while block.isValid(): data = block.userData() lists = [data.parentheses, data.braces, data.square_brackets] for symbols in lists: for paren in symbols: if paren.position < offset and down: continue if paren.position >= offset and not down: continue if self.is_paren_open(paren): parens[paren.character] += 1 rparens[rmatching[paren.character]] -= 1 if (operation == self._prev_block and rparens[rmatching[paren.character]] < 0): return True if self.is_paren_closed(paren): rparens[paren.character] += 1 parens[matching[paren.character]] -= 1 if (operation == self._next_block and parens[matching[paren.character]] < 0): return True block = operation(block) offset = 0 if down else len(block.text()) elif nb_open > 0: return True return False def _next_block(self, b): return b.next() def _prev_block(self, b): return b.previous() def is_in_comment(self, column, tc, full_line): use_parent_impl = False usd = tc.block().userData() for start, end in usd.cc_disabled_zones: if start < column < end: string = full_line[start:end] if not ((string.startswith("'") or string.startswith('"')) and (string.endswith("'") or string.endswith('"'))): use_parent_impl = True break return use_parent_impl def get_last_word(self, tc): tc2 = QTextCursor(tc) tc2.movePosition(QTextCursor.Left, 1) tc2.movePosition(QTextCursor.WordLeft, tc.KeepAnchor) # tc2.movePosition(QTextCursor.Right, tc.KeepAnchor, # self.editor.cursorPosition[1]) return tc2.selectedText().strip() def get_indent_of_opening_paren(self, tc, column): # find last closed paren pos = None data = tc.block().userData() tc2 = QTextCursor(tc) tc2.movePosition(tc2.StartOfLine, tc2.MoveAnchor) for paren in reversed(data.parentheses): if paren.character == ')': column = paren.position pos = tc2.position() + column + 1 break if pos: tc2 = QTextCursor(tc) tc2.setPosition(pos) ol, oc = self.editor.modes.get(SymbolMatcherMode).symbol_pos( tc2, '(', 0) line = self._helper.line_text(ol) return len(line) - len(line.lstrip()) return None def get_last_open_paren_pos(self, tc, column): pos = None char = None ln = tc.blockNumber() + 1 tc_trav = QTextCursor(tc) while ln >= 1: tc_trav.movePosition(tc_trav.StartOfLine, tc_trav.MoveAnchor) data = tc_trav.block().userData() lists = [data.parentheses, data.braces, data.square_brackets] for symbols in lists: for paren in reversed(symbols): if paren.position < column: if self.is_paren_open(paren): if paren.position > column: continue else: pos = tc_trav.position() + paren.position char = paren.character # ensure it does not have a closing paren on # the same line tc3 = QTextCursor(tc) tc3.setPosition(pos) l, c = self.editor.modes.get( SymbolMatcherMode).symbol_pos(tc3, ')') if l == ln and c < column: continue return pos, char # check previous line tc_trav.movePosition(tc_trav.Up, tc_trav.MoveAnchor) ln = tc_trav.blockNumber() + 1 column = len(self._helper.line_text(ln)) return pos, char def get_parent_pos(self, tc, column): pos, char = self.get_last_open_paren_pos(tc, column) if char == '(': ptype = 0 closingchar = ')' elif char == '[': ptype = 1 closingchar = ']' elif char == '{': ptype = 2 closingchar = '}' tc2 = QTextCursor(tc) tc2.setPosition(pos) ol, oc = self.editor.modes.get(SymbolMatcherMode).symbol_pos( tc2, char, ptype) cl, cc = self.editor.modes.get(SymbolMatcherMode).symbol_pos( tc2, closingchar, ptype) return (ol, oc), (cl, cc) def get_next_char(self, tc): tc2 = QTextCursor(tc) tc2.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) char = tc2.selectedText() return char def handle_indent_after_paren(self, column, line, fullline, tc): """ Handle indent between symbols such as parenthesis, braces,... """ # elements might be separated by ',' 'or' 'and' next_char = self.get_next_char(tc) nextcharisclosingsymbol = next_char in [']', ')', '}'] (oL, oC), (cL, cC) = self.get_parent_pos(tc, column) closingline = self._helper.line_text(cL) openingline = self._helper.line_text(oL) openingline = re.sub(r'".*"', "", openingline) openingline = re.sub(r"'.*'", "", openingline) openingindent = len(openingline) - len(openingline.lstrip()) tokens = [t.strip() for t in re.split(', |and |or ', line[oC:column]) if t] # align with first token pos if len(closingline) > cC and closingline[cC] == ":": post = openingindent * " " + 8 * " " else: # press enter before a '}', ']', ')' # which close an affectation (tuple, list , dict) if nextcharisclosingsymbol and re.match('.*=[\s][\W].*', openingline): post = openingindent * " " else: # align elems in list, tuple, dict if re.match('.*=[\s][\W].*', openingline) and oL - cL == 1: post = openingindent * " " + 4 * " " # align elems in fct declaration (we align with first # token) else: if len(tokens): post = oC * " " else: post = openingindent * " " + 4 * " " pre = "" in_string_def, char = self.is_in_string_def(fullline, column) if in_string_def: pre = char post += char return pre, post def at_block_start(self, tc, line): """ Improve QTextCursor.atBlockStart to ignore spaces """ if tc.atBlockStart(): return True column = tc.columnNumber() indentation = len(line) - len(line.lstrip()) return column <= indentation def at_block_end(self, tc, fullline): if tc.atBlockEnd(): return True column = tc.columnNumber() return column >= len(fullline.rstrip()) - 1 def _get_indent(self, cursor): pos = cursor.position() ln, column = self._helper.cursor_position() fullline = self.get_full_line(cursor) line = fullline[:column] # no indent if pos == 0 or column == 0: return "", "" pre, post = AutoIndentMode._get_indent(self, cursor) if self.at_block_start(cursor, line): if self.has_two_empty_line_before(cursor): post = post[:-4] return pre, post # return pressed in comments if self.is_in_comment(column, cursor, fullline): if line.strip().startswith("#") and column != len(fullline): post += '# ' return pre, post elif self.between_paren(cursor, column): try: pre, post = self.handle_indent_after_paren( column, line, fullline, cursor) except TypeError: return pre, post else: lastword = self.get_last_word(cursor) inStringDef, char = self.is_in_string_def(fullline, column) if inStringDef: # the string might be between paren if multiline # check if there a at least a non closed paren on the previous # lines if self.has_unclosed_paren(cursor): pre = char else: pre = '" \\' post += 4 * ' ' if fullline.endswith(':'): post += 4 * " " post += char elif fullline.rstrip().endswith(":") and \ lastword.rstrip().endswith(':') and \ self.at_block_end(cursor, fullline): try: indent = (self.get_indent_of_opening_paren(cursor, column) + 4) if indent: post = indent * " " except TypeError: kw = ["if", "class", "def", "while", "for", "else", "elif", "except", "finally", "try"] l = fullline ln = cursor.blockNumber() def check_kw_in_line(kwds, lparam): for kwd in kwds: if kwd in lparam: return True return False while not check_kw_in_line(kw, l) and ln: ln -= 1 l = self._helper.line_text(ln) indent = (len(l) - len(l.lstrip())) * " " indent += 4 * " " post = indent elif line.endswith("\\"): # increment indent post += 4 * " " elif fullline.endswith(")") and lastword.endswith(')'): # find line where the open braces can be found and align with # that line indent = self.get_indent_of_opening_paren(cursor, column) if indent: post = indent * " " elif ("\\" not in fullline and "#" not in fullline and fullline.strip() and not fullline.endswith(')') and not self.at_block_end(cursor, fullline)): if lastword and lastword[-1] != " ": pre += " \\" else: pre += '\\' post += 4 * " " if fullline.endswith(':'): post += 4 * " " elif (lastword == "return" or lastword == "pass"): post = post[:-4] return pre, post
def on_install(self, editor): super().on_install(editor) self._helper = TextHelper(editor)
class PyAutoIndentMode(AutoIndentMode): """ Automatically indents text, respecting the PEP8 conventions. Customised :class:`pyqode.core.modes.AutoIndentMode` for python that tries its best to follow the pep8 indentation guidelines. """ def __init__(self): super(PyAutoIndentMode, self).__init__() self._helper = None def on_install(self, editor): super(PyAutoIndentMode, self).on_install(editor) self._helper = TextHelper(editor) def _get_indent(self, cursor): ln, column = self._helper.cursor_position() fullline = self._get_full_line(cursor) line = fullline[:column] pre, post = AutoIndentMode._get_indent(self, cursor) if self._at_block_start(cursor, line): return pre, post # return pressed in comments c2 = QTextCursor(cursor) if c2.atBlockEnd(): c2.movePosition(c2.Left) if (self._helper.is_comment_or_string( c2, formats=['comment', 'docstring']) or fullline.endswith('"""')): if line.strip().startswith("#") and column != len(fullline): post += '# ' return pre, post # between parens elif self._between_paren(cursor, column): return self._handle_indent_between_paren( column, line, (pre, post), cursor) else: lastword = self._get_last_word(cursor) lastwordu = self._get_last_word_unstripped(cursor) in_string_def, char = self._is_in_string_def(fullline, column) if in_string_def: post, pre = self._handle_indent_inside_string( char, cursor, fullline, post) elif (fullline.rstrip().endswith(":") and lastword.rstrip().endswith(':') and self._at_block_end(cursor, fullline)): post = self._handle_new_scope_indentation( cursor, fullline) elif line.endswith("\\"): # if user typed \ and press enter -> indent is always # one level higher post += self.editor.tab_length * " " elif (fullline.endswith((')', '}', ']')) and lastword.endswith((')', '}', ']'))): post = self._handle_indent_after_paren(cursor, post) elif ("\\" not in fullline and "#" not in fullline and not self._at_block_end(cursor, fullline)): post, pre = self._handle_indent_in_statement( fullline, lastwordu, post, pre) elif ((self._at_block_end(cursor, fullline) and fullline.strip().startswith('return ')) or lastword == "pass"): post = post[:-self.editor.tab_length] return pre, post @staticmethod def _is_in_string_def(full_line, column): count = 0 char = "'" for i in range(len(full_line)): if full_line[i] == "'" or full_line[i] == '"': count += 1 if full_line[i] == '"' and i < column: char = '"' count_after_col = 0 for i in range(column, len(full_line)): if full_line[i] == "'" or full_line[i] == '"': count_after_col += 1 return count % 2 == 0 and count_after_col % 2 == 1, char @staticmethod def _is_paren_open(paren): return (paren.character == "(" or paren.character == "[" or paren.character == '{') @staticmethod def _is_paren_closed(paren): return (paren.character == ")" or paren.character == "]" or paren.character == '}') @staticmethod def _get_full_line(tc): tc2 = QTextCursor(tc) tc2.select(QTextCursor.LineUnderCursor) full_line = tc2.selectedText() return full_line def _parens_count_for_block(self, col, block): open_p = [] closed_p = [] lists = get_block_symbol_data(self.editor, block) for symbols in lists: for paren in symbols: if paren.position >= col: continue if self._is_paren_open(paren): if not col: return -1, -1, [], [] open_p.append(paren) if self._is_paren_closed(paren): closed_p.append(paren) return len(open_p), len(closed_p), open_p, closed_p def _between_paren(self, tc, col): try: self.editor.modes.get('SymbolMatcherMode') except KeyError: return False block = tc.block() nb_open = nb_closed = 0 while block.isValid() and block.text().strip(): o, c, _, _ = self._parens_count_for_block(col, block) nb_open += o nb_closed += c block = block.previous() col = len(block.text()) return nb_open > nb_closed @staticmethod def _get_last_word(tc): tc2 = QTextCursor(tc) tc2.movePosition(QTextCursor.Left, tc.KeepAnchor, 1) tc2.movePosition(QTextCursor.WordLeft, tc.KeepAnchor) return tc2.selectedText().strip() @staticmethod def _get_last_word_unstripped(tc): tc2 = QTextCursor(tc) tc2.movePosition(QTextCursor.Left, tc.KeepAnchor, 1) tc2.movePosition(QTextCursor.WordLeft, tc.KeepAnchor) return tc2.selectedText() def _get_indent_of_opening_paren(self, tc): tc.movePosition(tc.Left, tc.KeepAnchor) char = tc.selectedText() tc.movePosition(tc.Right, tc.MoveAnchor) mapping = { ')': (OPEN, PAREN), ']': (OPEN, SQUARE), '}': (OPEN, BRACE) } try: character, char_type = mapping[char] except KeyError: return None else: ol, oc = self.editor.modes.get(SymbolMatcherMode).symbol_pos( tc, character, char_type) line = self._helper.line_text(ol) return len(line) - len(line.lstrip()) def _get_first_open_paren(self, tc, column): pos = None char = None ln = tc.blockNumber() tc_trav = QTextCursor(tc) mapping = { '(': (CLOSE, PAREN), '[': (CLOSE, SQUARE), '{': (CLOSE, BRACE) } while ln >= 0 and tc.block().text().strip(): tc_trav.movePosition(tc_trav.StartOfLine, tc_trav.MoveAnchor) lists = get_block_symbol_data(self.editor, tc_trav.block()) all_symbols = [] for symbols in lists: all_symbols += [s for s in symbols] symbols = sorted(all_symbols, key=lambda x: x.position) for paren in reversed(symbols): if paren.position < column: if self._is_paren_open(paren): if paren.position > column: continue else: pos = tc_trav.position() + paren.position char = paren.character # ensure it does not have a closing paren on # the same line tc3 = QTextCursor(tc) tc3.setPosition(pos) try: ch, ch_type = mapping[paren.character] l, c = self.editor.modes.get( SymbolMatcherMode).symbol_pos( tc3, ch, ch_type) except KeyError: continue if l == ln and c < column: continue return pos, char # check previous line tc_trav.movePosition(tc_trav.Up, tc_trav.MoveAnchor) ln = tc_trav.blockNumber() column = len(self._helper.line_text(ln)) return pos, char def _get_paren_pos(self, tc, column): pos, char = self._get_first_open_paren(tc, column) mapping = {'(': PAREN, '[': SQUARE, '{': BRACE} tc2 = QTextCursor(tc) tc2.setPosition(pos) import sys ol, oc = self.editor.modes.get(SymbolMatcherMode).symbol_pos( tc2, OPEN, mapping[char]) cl, cc = self.editor.modes.get(SymbolMatcherMode).symbol_pos( tc2, CLOSE, mapping[char]) return (ol, oc), (cl, cc) @staticmethod def _get_next_char(tc): tc2 = QTextCursor(tc) tc2.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) char = tc2.selectedText() return char @staticmethod def _get_prev_char(tc): tc2 = QTextCursor(tc) tc2.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor) char = tc2.selectedText() while char == ' ': tc2.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor) char = tc2.selectedText() return char.strip() def _handle_indent_between_paren(self, column, line, parent_impl, tc): """ Handle indent between symbols such as parenthesis, braces,... """ pre, post = parent_impl next_char = self._get_next_char(tc) prev_char = self._get_prev_char(tc) prev_open = prev_char in ['[', '(', '{'] next_close = next_char in [']', ')', '}'] (open_line, open_symbol_col), (close_line, close_col) = \ self._get_paren_pos(tc, column) open_line_txt = self._helper.line_text(open_line) open_line_indent = len(open_line_txt) - len(open_line_txt.lstrip()) if prev_open: post = (open_line_indent + self.editor.tab_length) * ' ' elif next_close and prev_char != ',': post = open_line_indent * ' ' elif tc.block().blockNumber() == open_line: post = open_symbol_col * ' ' # adapt indent if cursor on closing line and next line have same # indent -> PEP8 compliance if close_line and close_col: txt = self._helper.line_text(close_line) bn = tc.block().blockNumber() flg = bn == close_line next_indent = self._helper.line_indent(bn + 1) * ' ' if flg and txt.strip().endswith(':') and next_indent == post: # | look at how the previous line ( ``':'):`` ) was # over-indented, this is actually what we are trying to # achieve here post += self.editor.tab_length * ' ' # breaking string if next_char in ['"', "'"]: tc.movePosition(tc.Left) is_string = self._helper.is_comment_or_string(tc, formats=['string']) if next_char in ['"', "'"]: tc.movePosition(tc.Right) if is_string: trav = QTextCursor(tc) while self._helper.is_comment_or_string( trav, formats=['string']): trav.movePosition(trav.Left) trav.movePosition(trav.Right) symbol = '%s' % self._get_next_char(trav) pre += symbol post += symbol return pre, post @staticmethod def _at_block_start(tc, line): """ Improve QTextCursor.atBlockStart to ignore spaces """ if tc.atBlockStart(): return True column = tc.columnNumber() indentation = len(line) - len(line.lstrip()) return column <= indentation @staticmethod def _at_block_end(tc, fullline): if tc.atBlockEnd(): return True column = tc.columnNumber() return column >= len(fullline.rstrip()) - 1 def _handle_indent_inside_string(self, char, cursor, fullline, post): # break string with a '\' at the end of the original line, always # breaking strings enclosed by parens is done in the # _handle_between_paren method n = self.editor.tab_length pre = '%s \\' % char post += n * ' ' if fullline.endswith(':'): post += n * " " post += char return post, pre def _handle_new_scope_indentation(self, cursor, fullline): try: indent = (self._get_indent_of_opening_paren(cursor) + self.editor.tab_length) post = indent * " " except TypeError: # e.g indent is None (meaning the line does not ends with ):, ]: # or }: kw = ["if", "class", "def", "while", "for", "else", "elif", "except", "finally", "try"] l = fullline ln = cursor.blockNumber() def check_kw_in_line(kwds, lparam): for kwd in kwds: if kwd in lparam: return True return False while not check_kw_in_line(kw, l) and ln: ln -= 1 l = self._helper.line_text(ln) indent = (len(l) - len(l.lstrip())) * " " indent += self.editor.tab_length * " " post = indent return post def _handle_indent_after_paren(self, cursor, post): indent = self._get_indent_of_opening_paren(cursor) if indent is not None: post = indent * " " return post def _handle_indent_in_statement(self, fullline, lastword, post, pre): if lastword and lastword[-1] != " ": pre += " \\" else: pre += '\\' post += self.editor.tab_length * " " if fullline.endswith(':'): post += self.editor.tab_length * " " return post, pre
def on_install(self, editor): super(PyAutoIndentMode, self).on_install(editor) self._helper = TextHelper(editor)