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

b0381031facd0b97cf8fa502c168c4d26731d850
[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 " TODO: check required patch for timeout argument, likely lower than 7.3.429 though.
42 if !exists('g:python_pep8_indent_searchpair_timeout')
43     if has('patch-8.0.1483')
44         let g:python_pep8_indent_searchpair_timeout = 150
45     else
46         let g:python_pep8_indent_searchpair_timeout = 0
47     endif
48 endif
49
50 let s:block_rules = {
51             \ '^\s*elif\>': ['if', 'elif'],
52             \ '^\s*except\>': ['try', 'except'],
53             \ '^\s*finally\>': ['try', 'except', 'else']
54             \ }
55 let s:block_rules_multiple = {
56             \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
57             \ }
58 " Pairs to look for when searching for opening parenthesis.
59 " The value is the maximum offset in lines.
60 let s:paren_pairs = {'()': 50, '[]': 100, '{}': 1000}
61
62 if &filetype ==# 'pyrex' || &filetype ==# 'cython'
63     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
64 else
65     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
66 endif
67 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
68
69 " Skip strings and comments. Return 1 for chars to skip.
70 " jedi* refers to syntax definitions from jedi-vim for call signatures, which
71 " are inserted temporarily into the buffer.
72 let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
73             \ '=~? "\\vstring|comment|^pythonbytes%(contents)=$|jedi\\S"'
74
75 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
76             \ '=~? "\\vcomment|jedi\\S"'
77
78 " Also ignore anything concealed.
79 " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
80 function! s:is_concealed(line, col)
81     let concealed = synconcealed(a:line, a:col)
82     return len(concealed) && concealed[0]
83 endfunction
84 if has('conceal')
85     let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
86 endif
87
88
89 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
90             \ '=~? "comment"'
91
92 " Use 'shiftwidth()' instead of '&sw'.
93 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
94 if exists('*shiftwidth')
95     function! s:sw()
96         return shiftwidth()
97     endfunction
98 else
99     function! s:sw()
100         return &shiftwidth
101     endfunction
102 endif
103
104 " Find backwards the closest open parenthesis/bracket/brace.
105 function! s:find_opening_paren(...)
106     " optional arguments: line and column (defaults to 1) to search around
107     if a:0 > 0
108         let view = winsaveview()
109         call cursor(a:1, a:0 > 1 ? a:2 : 1)
110         let ret = s:find_opening_paren()
111         call winrestview(view)
112         return ret
113     endif
114
115     " Return if cursor is in a comment.
116     exe 'if' s:skip_search '| return [0, 0] | endif'
117
118     let nearest = [0, 0]
119     for [p, maxoff] in items(s:paren_pairs)
120         let stopline = max([0, line('.') - maxoff, nearest[0]])
121         let next = searchpairpos(
122            \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline, g:python_pep8_indent_searchpair_timeout)
123         if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
124             let nearest = next
125         endif
126     endfor
127     return nearest
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 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             if getline(lnum) =~# re
157                 if !a:multiple
158                     return [indent]
159                 endif
160                 if index(r, indent) == -1
161                     let r += [indent]
162                 endif
163                 let last_indent = indent
164             endif
165         endif
166         let lnum = prevnonblank(lnum - 1)
167     endwhile
168     return r
169 endfunction
170
171 " Is "expr" true for every position in "lnum", beginning at "start"?
172 " (optionally up to a:1 / 4th argument)
173 function! s:match_expr_on_line(expr, lnum, start, ...)
174     let text = getline(a:lnum)
175     let end = a:0 ? a:1 : len(text)
176     if a:start > end
177         return 1
178     endif
179     let save_pos = getpos('.')
180     let r = 1
181     for i in range(a:start, end)
182         call cursor(a:lnum, i)
183         if !(eval(a:expr) || text[i-1] =~# '\s')
184             let r = 0
185             break
186         endif
187     endfor
188     call setpos('.', save_pos)
189     return r
190 endfunction
191
192 " Line up with open parenthesis/bracket/brace.
193 function! s:indent_like_opening_paren(lnum)
194     let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum)
195     if paren_lnum <= 0
196         return -2
197     endif
198     let text = getline(paren_lnum)
199     let base = indent(paren_lnum)
200
201     let nothing_after_opening_paren = s:match_expr_on_line(
202                 \ s:skip_after_opening_paren, paren_lnum, paren_col+1)
203     let starts_with_closing_paren = getline(a:lnum) =~# '^\s*[])}]'
204
205     let hang_closing = get(b:, 'python_pep8_indent_hang_closing',
206                 \ get(g:, 'python_pep8_indent_hang_closing', 0))
207
208     if nothing_after_opening_paren
209         if starts_with_closing_paren && !hang_closing
210             let res = base
211         else
212             let res = base + s:sw()
213         endif
214     else
215         " Indent to match position of opening paren.
216         let res = paren_col
217     endif
218
219     " If this line is the continuation of a control statement
220     " indent further to distinguish the continuation line
221     " from the next logical line.
222     if text =~# b:control_statement && res == base + s:sw()
223         " But only if not inside parens itself (Flake's E127).
224         let [paren_lnum, _] = s:find_opening_paren(paren_lnum)
225         if paren_lnum <= 0
226             return res + s:sw()
227         endif
228     endif
229     return res
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     let empty = getline(a:lnum) =~# '^\s*$'
307
308     " Current and prev line are empty, next is not -> indent like next.
309     if empty && a:lnum > 1 &&
310           \ (getline(a:lnum - 1) =~# '^\s*$') &&
311           \ !(getline(a:lnum + 1) =~# '^\s*$')
312       return indent(a:lnum + 1)
313     endif
314
315     " If the previous statement was a stop-execution statement or a pass
316     if getline(start) =~# s:stop_statement
317         " Remove one level of indentation if the user hasn't already dedented
318         if empty || current > base - s:sw()
319             return base - s:sw()
320         endif
321         " Otherwise, trust the user
322         return -1
323     endif
324
325     if !empty && s:is_dedented_already(current, base)
326         return -1
327     endif
328
329     " In all other cases, line up with the start of the previous statement.
330     return base
331 endfunction
332
333 " If this line is dedented and the number of indent spaces is valid
334 " (multiple of the indentation size), trust the user.
335 function! s:is_dedented_already(current, base)
336     let dedent_size = a:current - a:base
337     return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
338 endfunction
339
340 " Is the syntax at lnum (and optionally cnum) a python string?
341 function! s:is_python_string(lnum, ...)
342     let line = getline(a:lnum)
343     if a:0
344       let cols = type(a:1) != type([]) ? [a:1] : a:1
345     else
346       let cols = range(1, max([1, len(line)]))
347     endif
348     for cnum in cols
349         if match(map(synstack(a:lnum, cnum),
350                     \ "synIDattr(v:val, 'name')"), 'python\S*String') == -1
351             return 0
352         end
353     endfor
354     return 1
355 endfunction
356
357 function! GetPythonPEPIndent(lnum)
358     " First line has indent 0
359     if a:lnum == 1
360         return 0
361     endif
362
363     let line = getline(a:lnum)
364     let prevline = getline(a:lnum-1)
365
366     " Multilinestrings: continous, docstring or starting.
367     if s:is_python_string(a:lnum-1, max([1, len(prevline)]))
368                 \ && (s:is_python_string(a:lnum, 1)
369                 \     || match(line, '^\%("""\|''''''\)') != -1)
370
371         " Indent closing quotes as the line with the opening ones.
372         let match_quotes = match(line, '^\s*\zs\%("""\|''''''\)')
373         if match_quotes != -1
374             " closing multiline string
375             let quotes = line[match_quotes:(match_quotes+2)]
376             let pairpos = searchpairpos(quotes, '', quotes, 'b', 1, g:python_pep8_indent_searchpair_timeout)
377             if pairpos[0] != 0
378                 return indent(pairpos[0])
379             else
380                 " TODO: test to cover this!
381             endif
382         endif
383
384         if s:is_python_string(a:lnum-1)
385             " Previous line is (completely) a string: keep current indent.
386             return -1
387         endif
388
389         if match(prevline, '^\s*\%("""\|''''''\)') != -1
390             " docstring.
391             return indent(a:lnum-1)
392         endif
393
394         let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
395                     \ get(g:, 'python_pep8_indent_multiline_string', 0))
396         if match(prevline, '\v%("""|'''''')$') != -1
397             " Opening multiline string, started in previous line.
398             if (&autoindent && indent(a:lnum) == indent(a:lnum-1))
399                         \ || match(line, '\v^\s+$') != -1
400                 " <CR> with empty line or to split up 'foo("""bar' into
401                 " 'foo("""' and 'bar'.
402                 if indent_multi == -2
403                     return indent(a:lnum-1) + s:sw()
404                 endif
405                 return indent_multi
406             endif
407         endif
408
409         " Keep existing indent.
410         if match(line, '\v^\s*\S') != -1
411             return -1
412         endif
413
414         if indent_multi != -2
415             return indent_multi
416         endif
417
418         return s:indent_like_opening_paren(a:lnum)
419     endif
420
421     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
422     let indent = s:indent_like_opening_paren(a:lnum)
423     if indent >= -1
424         return indent
425     endif
426
427     " Blocks: Match indent of first block of this type.
428     let indent = s:indent_like_block(a:lnum)
429     if indent >= -1
430         return indent
431     endif
432
433     return s:indent_like_previous_line(a:lnum)
434 endfunction