1import re
2
3from rope.base import codeanalyze
4
5
6class TextIndenter(object):
7    """A class for formatting texts"""
8
9    def __init__(self, editor, indents=4):
10        self.editor = editor
11        self.indents = indents
12        self.line_editor = editor.line_editor()
13
14    def correct_indentation(self, lineno):
15        """Correct the indentation of a line"""
16
17    def deindent(self, lineno):
18        """Deindent the a line"""
19        current_indents = self._count_line_indents(lineno)
20        new_indents = max(0, current_indents - self.indents)
21        self._set_line_indents(lineno, new_indents)
22
23    def indent(self, lineno):
24        """Indent a line"""
25        current_indents = self._count_line_indents(lineno)
26        new_indents = current_indents + self.indents
27        self._set_line_indents(lineno, new_indents)
28
29    def entering_new_line(self, lineno):
30        """Indent a line
31
32        Uses `correct_indentation` and last line indents
33        """
34        last_line = ""
35        if lineno > 1:
36            last_line = self.line_editor.get_line(lineno - 1)
37        if last_line.strip() == '':
38            self._set_line_indents(lineno, len(last_line))
39        else:
40            self.correct_indentation(lineno)
41
42    def insert_tab(self, index):
43        """Inserts a tab in the given index"""
44        self.editor.insert(index, ' ' * self.indents)
45
46    def _set_line_indents(self, lineno, indents):
47        old_indents = self._count_line_indents(lineno)
48        indent_diffs = indents - old_indents
49        self.line_editor.indent_line(lineno, indent_diffs)
50
51    def _count_line_indents(self, lineno):
52        contents = self.line_editor.get_line(lineno)
53        result = 0
54        for x in contents:
55            if x == ' ':
56                result += 1
57            elif x == '\t':
58                result += 8
59            else:
60                break
61        return result
62
63
64class NormalIndenter(TextIndenter):
65
66    def __init__(self, editor):
67        super(NormalIndenter, self).__init__(editor)
68
69    def correct_indentation(self, lineno):
70        prev_indents = 0
71        if lineno > 1:
72            prev_indents = self._count_line_indents(lineno - 1)
73        self._set_line_indents(lineno, prev_indents)
74
75
76class PythonCodeIndenter(TextIndenter):
77
78    def __init__(self, editor, indents=4):
79        super(PythonCodeIndenter, self).__init__(editor, indents)
80
81    def _last_non_blank(self, lineno):
82        current_line = lineno - 1
83        while current_line != 1 and \
84              self.line_editor.get_line(current_line).strip() == '':
85            current_line -= 1
86        return current_line
87
88    def _get_correct_indentation(self, lineno):
89        if lineno == 1:
90            return 0
91        new_indent = self._get_base_indentation(lineno)
92
93        prev_lineno = self._last_non_blank(lineno)
94        prev_line = self.line_editor.get_line(prev_lineno)
95        if prev_lineno == lineno or prev_line.strip() == '':
96            new_indent = 0
97        current_line = self.line_editor.get_line(lineno)
98        new_indent += self._indents_caused_by_current_stmt(current_line)
99        return new_indent
100
101    def _get_base_indentation(self, lineno):
102        range_finder = _StatementRangeFinder(
103            self.line_editor, self._last_non_blank(lineno))
104        start = range_finder.get_statement_start()
105        if not range_finder.is_line_continued():
106            changes = self._indents_caused_by_prev_stmt(
107                (start, self._last_non_blank(lineno)))
108            return self._count_line_indents(start) + changes
109        if range_finder.last_open_parens():
110            open_parens = range_finder.last_open_parens()
111            parens_line = self.line_editor.get_line(open_parens[0])
112            if parens_line[open_parens[1] + 1:].strip() == '':
113                if len(range_finder.open_parens) > 1:
114                    return range_finder.open_parens[-2][1] + 1
115                else:
116                    return self._count_line_indents(start) + self.indents
117            return range_finder.last_open_parens()[1] + 1
118
119        start_line = self.line_editor.get_line(start)
120        if start == lineno - 1:
121            try:
122                equals_index = start_line.index(' = ') + 1
123                if start_line[equals_index + 1:].strip() == '\\':
124                    return self._count_line_indents(start) + self.indents
125                return equals_index + 2
126            except ValueError:
127                match = re.search(r'(\b )|(\.)', start_line)
128                if match:
129                    return match.start() + 1
130                else:
131                    return len(start_line) + 1
132        else:
133            return self._count_line_indents(self._last_non_blank(lineno))
134
135    def _indents_caused_by_prev_stmt(self, stmt_range):
136        first_line = self.line_editor.get_line(stmt_range[0])
137        last_line = self.line_editor.get_line(stmt_range[1])
138        new_indent = 0
139        if self._strip(last_line).endswith(':'):
140            new_indent += self.indents
141        if self._startswith(first_line, ('return', 'raise', 'pass',
142                                         'break', 'continue')):
143            new_indent -= self.indents
144        return new_indent
145
146    def _startswith(self, line, tokens):
147        line = self._strip(line)
148        for token in tokens:
149            if line == token or line.startswith(token + ' '):
150                return True
151
152    def _strip(self, line):
153        try:
154            numsign = line.rindex('#')
155            comment = line[numsign:]
156            if '\'' not in comment and '\"' not in comment:
157                line = line[:numsign]
158        except ValueError:
159            pass
160        return line.strip()
161
162    def _indents_caused_by_current_stmt(self, current_line):
163        new_indent = 0
164        if self._strip(current_line) == 'else:':
165            new_indent -= self.indents
166        if self._strip(current_line) == 'finally:':
167            new_indent -= self.indents
168        if self._startswith(current_line, ('elif',)):
169            new_indent -= self.indents
170        if self._startswith(current_line, ('except',)) and \
171           self._strip(current_line).endswith(':'):
172            new_indent -= self.indents
173        return new_indent
174
175    def correct_indentation(self, lineno):
176        """Correct the indentation of the line containing the given index"""
177        self._set_line_indents(lineno, self._get_correct_indentation(lineno))
178
179
180class _StatementRangeFinder(object):
181    """A method object for finding the range of a statement"""
182
183    def __init__(self, lines, lineno):
184        self.lines = lines
185        self.lineno = lineno
186        self.in_string = ''
187        self.open_count = 0
188        self.explicit_continuation = False
189        self.open_parens = []
190        self._analyze()
191
192    def _analyze_line(self, lineno):
193        current_line = self.lines.get_line(lineno)
194        for i, char in enumerate(current_line):
195            if char in '\'"':
196                if self.in_string == '':
197                    self.in_string = char
198                    if char * 3 == current_line[i:i + 3]:
199                        self.in_string = char * 3
200                elif self.in_string == current_line[i:i + len(self.in_string)] and \
201                     not (i > 0 and current_line[i - 1] == '\\' and
202                          not (i > 1 and current_line[i - 2:i] == '\\\\')):
203                    self.in_string = ''
204            if self.in_string != '':
205                continue
206            if char == '#':
207                break
208            if char in '([{':
209                self.open_count += 1
210                self.open_parens.append((lineno, i))
211            if char in ')]}':
212                self.open_count -= 1
213                if self.open_parens:
214                    self.open_parens.pop()
215        if current_line and char != '#' and current_line.endswith('\\'):
216            self.explicit_continuation = True
217        else:
218            self.explicit_continuation = False
219
220    def _analyze(self):
221        last_statement = 1
222        block_start = codeanalyze.get_block_start(self.lines, self.lineno)
223        for current_line_number in range(block_start, self.lineno + 1):
224            if not self.explicit_continuation and \
225               self.open_count == 0 and self.in_string == '':
226                last_statement = current_line_number
227            self._analyze_line(current_line_number)
228        self.statement_start = last_statement
229
230    def get_statement_start(self):
231        return self.statement_start
232
233    def last_open_parens(self):
234        if not self.open_parens:
235            return None
236        return self.open_parens[-1]
237
238    def is_line_continued(self):
239        return self.open_count != 0 or self.explicit_continuation
240
241    def get_line_indents(self, line_number):
242        return self._count_line_indents(self.lines.get_line(line_number))
243