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

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