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): """ 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
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
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