]> git.madduck.net Git - etc/vim.git/blob - indent/python.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Fix indentation of "else" in nested "if" (#59)
[etc/vim.git] / indent / python.vim
1 " PEP8 compatible Python indent file
2 " Language:         Python
3 " Maintainer:       Hynek Schlawack <hs@ox.cx>
4 " Prev Maintainer:  Eric Mc Sween <em@tomcom.de> (address invalid)
5 " Original Author:  David Bustos <bustos@caltech.edu> (address invalid)
6 " License:          CC0
7 "
8 " vim-python-pep8-indent - A nicer Python indentation style for vim.
9 " Written in 2004 by David Bustos <bustos@caltech.edu>
10 " Maintained from 2004-2005 by Eric Mc Sween <em@tomcom.de>
11 " Maintained from 2013 by Hynek Schlawack <hs@ox.cx>
12 "
13 " To the extent possible under law, the author(s) have dedicated all copyright
14 " and related and neighboring rights to this software to the public domain
15 " worldwide. This software is distributed without any warranty.
16 " You should have received a copy of the CC0 Public Domain Dedication along
17 " with this software. If not, see
18 " <http://creativecommons.org/publicdomain/zero/1.0/>.
19
20 " Only load this indent file when no other was loaded.
21 if exists("b:did_indent")
22     finish
23 endif
24 let b:did_indent = 1
25
26 setlocal expandtab
27 setlocal nolisp
28 setlocal autoindent
29 setlocal indentexpr=GetPythonPEPIndent(v:lnum)
30 setlocal indentkeys=!^F,o,O,<:>,0),0],0},=elif,=except
31 setlocal tabstop=4
32 setlocal softtabstop=4
33 setlocal shiftwidth=4
34
35 if !exists('g:python_pep8_indent_multiline_string')
36     let g:python_pep8_indent_multiline_string = 0
37 endif
38
39 let s:maxoff = 50
40 let s:block_rules = {
41             \ '^\s*elif\>': ['if', 'elif'],
42             \ '^\s*except\>': ['try', 'except'],
43             \ '^\s*finally\>': ['try', 'except', 'else']
44             \ }
45 let s:block_rules_multiple = {
46             \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
47             \ }
48 let s:paren_pairs = ['()', '{}', '[]']
49 if &ft == 'pyrex' || &ft == 'cython'
50     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
51 else
52     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
53 endif
54 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
55
56 " Skip strings and comments. Return 1 for chars to skip.
57 " jedi* refers to syntax definitions from jedi-vim for call signatures, which
58 " are inserted temporarily into the buffer.
59 let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
60             \ '=~? "\\vstring|comment|jedi\\S"'
61
62 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
63             \ '=~? "\\vcomment|jedi\\S"'
64
65 " Also ignore anything concealed.
66 " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
67 function! s:is_concealed(line, col)
68     let concealed = synconcealed(a:line, a:col)
69     return len(concealed) && concealed[0]
70 endfunction
71 if has('conceal')
72     let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
73 endif
74
75
76 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
77             \ '=~? "comment"'
78
79 " Use 'shiftwidth()' instead of '&sw'.
80 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
81 if exists('*shiftwidth')
82     function! s:sw()
83         return shiftwidth()
84     endfunction
85 else
86     function! s:sw()
87         return &sw
88     endfunction
89 endif
90
91 function! s:pair_sort(x, y)
92     if a:x[0] == a:y[0]
93         return a:x[1] == a:y[1] ? 0 : a:x[1] > a:y[1] ? 1 : -1
94     else
95         return a:x[0] > a:y[0] ? 1 : -1
96     endif
97 endfunction
98
99 " Find backwards the closest open parenthesis/bracket/brace.
100 function! s:find_opening_paren(...)
101     " optional arguments: line and column (defaults to 1) to search around
102     if a:0 > 0
103         let view = winsaveview()
104         call cursor(a:1, a:0 > 1 ? a:2 : 1)
105         let ret = s:find_opening_paren()
106         call winrestview(view)
107         return ret
108     endif
109
110     let stopline = max([0, line('.') - s:maxoff])
111
112     " Return if cursor is in a comment.
113     exe 'if' s:skip_search '| return [0, 0] | endif'
114
115     let positions = []
116     for p in s:paren_pairs
117         call add(positions, searchpairpos(
118            \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline))
119     endfor
120
121     " Remove empty matches and return the type with the closest match
122     call filter(positions, 'v:val[0]')
123     call sort(positions, 's:pair_sort')
124
125     return get(positions, -1, [0, 0])
126 endfunction
127
128 " Find the start of a multi-line statement
129 function! s:find_start_of_multiline_statement(lnum)
130     let lnum = a:lnum
131     while lnum > 0
132         if getline(lnum - 1) =~ '\\$'
133             let lnum = prevnonblank(lnum - 1)
134         else
135             let [paren_lnum, _] = s:find_opening_paren(lnum)
136             if paren_lnum < 1
137                 return lnum
138             else
139                 let lnum = paren_lnum
140             endif
141         endif
142     endwhile
143 endfunction
144
145 " Find possible indent(s) of the block starter that matches the current line.
146 function! s:find_start_of_block(lnum, types, multiple)
147     let r = []
148     let types = copy(a:types)
149     let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
150     let lnum = a:lnum
151     let last_indent = indent(lnum) + 1
152     while lnum > 0 && last_indent > 0
153         let indent = indent(lnum)
154         if indent < last_indent
155             for type in types
156                 let re = '\v^\s*'.type.'>'
157                 if getline(lnum) =~# re
158                     if !a:multiple
159                         return [indent]
160                     endif
161                     if index(r, indent) == -1
162                         let r += [indent]
163                     endif
164                     " Remove any handled type, e.g. 'if'.
165                     call remove(types, index(types, type))
166                 endif
167             endfor
168             let last_indent = indent(lnum)
169         endif
170         let lnum = prevnonblank(lnum - 1)
171     endwhile
172     return r
173 endfunction
174
175 " Is "expr" true for every position in "lnum", beginning at "start"?
176 " (optionally up to a:1 / 4th argument)
177 function! s:match_expr_on_line(expr, lnum, start, ...)
178     let text = getline(a:lnum)
179     let end = a:0 ? a:1 : len(text)
180     if a:start > end
181         return 1
182     endif
183     let save_pos = getpos('.')
184     let r = 1
185     for i in range(a:start, end)
186         call cursor(a:lnum, i)
187         if !(eval(a:expr) || text[i-1] =~ '\s')
188             let r = 0
189             break
190         endif
191     endfor
192     call setpos('.', save_pos)
193     return r
194 endfunction
195
196 " Line up with open parenthesis/bracket/brace.
197 function! s:indent_like_opening_paren(lnum)
198     let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum)
199     if paren_lnum <= 0
200         return -2
201     endif
202     let text = getline(paren_lnum)
203     let base = indent(paren_lnum)
204
205     let nothing_after_opening_paren = s:match_expr_on_line(
206                 \ s:skip_after_opening_paren, paren_lnum, paren_col+1)
207     let starts_with_closing_paren = getline(a:lnum) =~ '^\s*[])}]'
208
209     if nothing_after_opening_paren
210         if starts_with_closing_paren
211             let res = base
212         else
213             let res = base + s:sw()
214         endif
215     else
216         " Indent to match position of opening paren.
217         let res = paren_col
218     endif
219
220     " If this line is the continuation of a control statement
221     " indent further to distinguish the continuation line
222     " from the next logical line.
223     if text =~# b:control_statement && res == base + s:sw()
224         return base + s:sw() * 2
225     else
226         return res
227     endif
228 endfunction
229
230 " Match indent of first block of this type.
231 function! s:indent_like_block(lnum)
232     let text = getline(a:lnum)
233     for [multiple, block_rules] in [
234                 \ [0, s:block_rules],
235                 \ [1, s:block_rules_multiple]]
236         for [line_re, blocks] in items(block_rules)
237             if text !~# line_re
238                 continue
239             endif
240
241             let indents = s:find_start_of_block(a:lnum - 1, blocks, multiple)
242             if !len(indents)
243                 return -1
244             endif
245             if len(indents) == 1
246                 return indents[0]
247             endif
248
249             " Multiple valid indents, e.g. for 'else' with both try and if.
250             let indent = indent(a:lnum)
251             if index(indents, indent) != -1
252                 " The indent is valid, keep it.
253                 return indent
254             endif
255             " Fallback to the first/nearest one.
256             return indents[0]
257         endfor
258     endfor
259     return -2
260 endfunction
261
262 function! s:indent_like_previous_line(lnum)
263     let lnum = prevnonblank(a:lnum - 1)
264
265     " No previous line, keep current indent.
266     if lnum < 1
267       return -1
268     endif
269
270     let text = getline(lnum)
271     let start = s:find_start_of_multiline_statement(lnum)
272     let base = indent(start)
273     let current = indent(a:lnum)
274
275     " Jump to last character in previous line.
276     call cursor(lnum, len(text))
277     let ignore_last_char = eval(s:skip_special_chars)
278
279     " Search for final colon that is not inside something to be ignored.
280     while 1
281         let curpos = getpos(".")[2]
282         if curpos == 1 | break | endif
283         if eval(s:skip_special_chars) || text[curpos-1] =~ '\s'
284             normal! h
285             continue
286         elseif text[curpos-1] == ':'
287             return base + s:sw()
288         endif
289         break
290     endwhile
291
292     if text =~ '\\$' && !ignore_last_char
293         " If this line is the continuation of a control statement
294         " indent further to distinguish the continuation line
295         " from the next logical line.
296         if getline(start) =~# b:control_statement
297             return base + s:sw() * 2
298         endif
299
300         " Nest (other) explicit continuations only one level deeper.
301         return base + s:sw()
302     endif
303
304     " If the previous statement was a stop-execution statement or a pass
305     if getline(start) =~# s:stop_statement
306         " Remove one level of indentation if the user hasn't already dedented
307         if indent(a:lnum) > base - s:sw()
308             return base - s:sw()
309         endif
310         " Otherwise, trust the user
311         return -1
312     endif
313
314     if s:is_dedented_already(current, base)
315         return -1
316     endif
317
318     " In all other cases, line up with the start of the previous statement.
319     return base
320 endfunction
321
322 " If this line is dedented and the number of indent spaces is valid
323 " (multiple of the indentation size), trust the user.
324 function! s:is_dedented_already(current, base)
325     let dedent_size = a:current - a:base
326     return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
327 endfunction
328
329 " Is the syntax at lnum (and optionally cnum) a python string?
330 function! s:is_python_string(lnum, ...)
331     let line = getline(a:lnum)
332     let linelen = len(line)
333     if linelen < 1
334       let linelen = 1
335     endif
336     let cols = a:0 ? type(a:1) != type([]) ? [a:1] : a:1 : range(1, linelen)
337     for cnum in cols
338         if match(map(synstack(a:lnum, cnum),
339                     \ 'synIDattr(v:val,"name")'), 'python\S*String') == -1
340             return 0
341         end
342     endfor
343     return 1
344 endfunction
345
346 function! GetPythonPEPIndent(lnum)
347     " First line has indent 0
348     if a:lnum == 1
349         return 0
350     endif
351
352     " Multilinestrings: continous, docstring or starting.
353     if s:is_python_string(a:lnum, 1)
354                 \ && s:is_python_string(a:lnum-1, len(getline(a:lnum-1)))
355         " Keep existing indent.
356         if match(getline(a:lnum), '\v^\s*\S') != -1
357             return -1
358         endif
359
360         if s:is_python_string(a:lnum-1)
361             " Previous line is (completely) a string.
362             return indent(a:lnum-1)
363         endif
364
365         if match(getline(a:lnum-1), '^\s*\%("""\|''''''\)') != -1
366             " docstring.
367             return indent(a:lnum-1)
368         endif
369
370         let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
371                     \ get(g:, 'python_pep8_indent_multiline_string', 0))
372         if indent_multi != -2
373             return indent_multi
374         endif
375
376         if match(getline(a:lnum-1), '\v%("""|'''''')$') != -1
377             " Opening multiline string, started in previous line.
378             return indent(a:lnum-1) + s:sw()
379         endif
380         return s:indent_like_opening_paren(a:lnum)
381     endif
382
383     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
384     let indent = s:indent_like_opening_paren(a:lnum)
385     if indent >= -1
386         return indent
387     endif
388
389     " Blocks: Match indent of first block of this type.
390     let indent = s:indent_like_block(a:lnum)
391     if indent >= -1
392         return indent
393     endif
394
395     return s:indent_like_previous_line(a:lnum)
396 endfunction