]> 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:

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