cadquery-freecad-module/CadQuery/Libs/pyqode/python/modes/autoindent.py
2015-10-26 16:25:38 -04:00

371 lines
14 KiB
Python

# -*- coding: utf-8 -*-
""" Contains smart indent modes """
import re
from pyqode.core.api import TextHelper, get_block_symbol_data
from pyqode.qt.QtGui import QTextCursor
from pyqode.core.modes import AutoIndentMode, SymbolMatcherMode
from pyqode.core.modes.matcher import CLOSE, PAREN, SQUARE, BRACE, OPEN
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).rstrip()
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)
end_with_op = fullline.endswith(
('+', '-', '*', '/', '=', ' and', ' or', '%'))
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 fullline.endswith("\\") and
(end_with_op or
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(tc2.PreviousCharacter, tc2.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", "with"]
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