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

Use s:_skip_special_chars function
[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 = '(execute("sleep 100m") && 0) || synIDattr(synID(line("."), col("."), 0), "name") ' .
73 function! s:_skip_special_chars()
74     return synIDattr(synID(line('.'), col('.'), 0), 'name')
75             \ =~? "\\vstring|comment|^pythonbytes%(contents)=$|jedi\\S"
76 endfunction
77 let s:skip_special_chars = 's:_skip_special_chars()'
78
79 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
80             \ '=~? "\\vcomment|jedi\\S"'
81
82 " Also ignore anything concealed.
83 " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
84 function! s:is_concealed(line, col)
85     let concealed = synconcealed(a:line, a:col)
86     return len(concealed) && concealed[0]
87 endfunction
88 if has('conceal')
89     let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
90 endif
91
92
93 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
94             \ '=~? "comment"'
95
96 " Use 'shiftwidth()' instead of '&sw'.
97 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
98 if exists('*shiftwidth')
99     function! s:sw()
100         return shiftwidth()
101     endfunction
102 else
103     function! s:sw()
104         return &shiftwidth
105     endfunction
106 endif
107
108 " Find backwards the closest open parenthesis/bracket/brace.
109 function! s:find_opening_paren(...)
110     " optional arguments: line and column (defaults to 1) to search around
111     if a:0 > 0
112         let view = winsaveview()
113         call cursor(a:1, a:0 > 1 ? a:2 : 1)
114         let ret = s:find_opening_paren()
115         call winrestview(view)
116         return ret
117     endif
118
119     " Return if cursor is in a comment.
120     exe 'if' s:skip_search '| return [0, 0] | endif'
121
122     let nearest = [0, 0]
123     for [p, maxoff] in items(s:paren_pairs)
124         let stopline = max([0, line('.') - maxoff, nearest[0]])
125         let next = searchpairpos(
126            \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline, g:python_pep8_indent_searchpair_timeout)
127         if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
128             let nearest = next
129         endif
130     endfor
131     return nearest
132 endfunction
133
134 " Find the start of a multi-line statement
135 function! s:find_start_of_multiline_statement(lnum)
136     let lnum = a:lnum
137     while lnum > 0
138         if getline(lnum - 1) =~# '\\$'
139             let lnum = prevnonblank(lnum - 1)
140         else
141             let [paren_lnum, _] = s:find_opening_paren(lnum)
142             if paren_lnum < 1
143                 return lnum
144             else
145                 let lnum = paren_lnum
146             endif
147         endif
148     endwhile
149 endfunction
150
151 " Find possible indent(s) of the block starter that matches the current line.
152 function! s:find_start_of_block(lnum, types, multiple)
153     let r = []
154     let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
155     let lnum = a:lnum
156     let last_indent = indent(lnum) + 1
157     while lnum > 0 && last_indent > 0
158         let indent = indent(lnum)
159         if indent < last_indent
160             if getline(lnum) =~# re
161                 if !a:multiple
162                     return [indent]
163                 endif
164                 if index(r, indent) == -1
165                     let r += [indent]
166                 endif
167                 let last_indent = indent
168             endif
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     let hang_closing = get(b:, 'python_pep8_indent_hang_closing',
210                 \ get(g:, 'python_pep8_indent_hang_closing', 0))
211
212     if nothing_after_opening_paren
213         if starts_with_closing_paren && !hang_closing
214             let res = base
215         else
216             let res = base + s:sw()
217         endif
218     else
219         " Indent to match position of opening paren.
220         let res = paren_col
221     endif
222
223     " If this line is the continuation of a control statement
224     " indent further to distinguish the continuation line
225     " from the next logical line.
226     if text =~# b:control_statement && res == base + s:sw()
227         " But only if not inside parens itself (Flake's E127).
228         let [paren_lnum, _] = s:find_opening_paren(paren_lnum)
229         if paren_lnum <= 0
230             return res + s:sw()
231         endif
232     endif
233     return res
234 endfunction
235
236 " Match indent of first block of this type.
237 function! s:indent_like_block(lnum)
238     let text = getline(a:lnum)
239     for [multiple, block_rules] in [
240                 \ [0, s:block_rules],
241                 \ [1, s:block_rules_multiple]]
242         for [line_re, blocks] in items(block_rules)
243             if text !~# line_re
244                 continue
245             endif
246
247             let indents = s:find_start_of_block(a:lnum - 1, blocks, multiple)
248             if !len(indents)
249                 return -1
250             endif
251             if len(indents) == 1
252                 return indents[0]
253             endif
254
255             " Multiple valid indents, e.g. for 'else' with both try and if.
256             let indent = indent(a:lnum)
257             if index(indents, indent) != -1
258                 " The indent is valid, keep it.
259                 return indent
260             endif
261             " Fallback to the first/nearest one.
262             return indents[0]
263         endfor
264     endfor
265     return -2
266 endfunction
267
268 function! s:indent_like_previous_line(lnum)
269     let lnum = prevnonblank(a:lnum - 1)
270
271     " No previous line, keep current indent.
272     if lnum < 1
273       return -1
274     endif
275
276     let text = getline(lnum)
277     let start = s:find_start_of_multiline_statement(lnum)
278     let base = indent(start)
279     let current = indent(a:lnum)
280
281     " Jump to last character in previous line.
282     call cursor(lnum, len(text))
283     let ignore_last_char = eval(s:skip_special_chars)
284
285     " Search for final colon that is not inside something to be ignored.
286     while 1
287         let curpos = getpos('.')[2]
288         if curpos == 1 | break | endif
289         if eval(s:skip_special_chars) || text[curpos-1] =~# '\s'
290             normal! h
291             continue
292         elseif text[curpos-1] ==# ':'
293             return base + s:sw()
294         endif
295         break
296     endwhile
297
298     if text =~# '\\$' && !ignore_last_char
299         " If this line is the continuation of a control statement
300         " indent further to distinguish the continuation line
301         " from the next logical line.
302         if getline(start) =~# b:control_statement
303             return base + s:sw() * 2
304         endif
305
306         " Nest (other) explicit continuations only one level deeper.
307         return base + s:sw()
308     endif
309
310     let empty = getline(a:lnum) =~# '^\s*$'
311
312     " Current and prev line are empty, next is not -> indent like next.
313     if empty && a:lnum > 1 &&
314           \ (getline(a:lnum - 1) =~# '^\s*$') &&
315           \ !(getline(a:lnum + 1) =~# '^\s*$')
316       return indent(a:lnum + 1)
317     endif
318
319     " If the previous statement was a stop-execution statement or a pass
320     if getline(start) =~# s:stop_statement
321         " Remove one level of indentation if the user hasn't already dedented
322         if empty || current > base - s:sw()
323             return base - s:sw()
324         endif
325         " Otherwise, trust the user
326         return -1
327     endif
328
329     if !empty && s:is_dedented_already(current, base)
330         return -1
331     endif
332
333     " In all other cases, line up with the start of the previous statement.
334     return base
335 endfunction
336
337 " If this line is dedented and the number of indent spaces is valid
338 " (multiple of the indentation size), trust the user.
339 function! s:is_dedented_already(current, base)
340     let dedent_size = a:current - a:base
341     return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
342 endfunction
343
344 " Is the syntax at lnum (and optionally cnum) a python string?
345 function! s:is_python_string(lnum, ...)
346     let line = getline(a:lnum)
347     if a:0
348       let cols = type(a:1) != type([]) ? [a:1] : a:1
349     else
350       let cols = range(1, max([1, len(line)]))
351     endif
352     for cnum in cols
353         if match(map(synstack(a:lnum, cnum),
354                     \ "synIDattr(v:val, 'name')"), 'python\S*String') == -1
355             return 0
356         end
357     endfor
358     return 1
359 endfunction
360
361 function! GetPythonPEPIndent(lnum)
362     " First line has indent 0
363     if a:lnum == 1
364         return 0
365     endif
366
367     let line = getline(a:lnum)
368     let prevline = getline(a:lnum-1)
369
370     " Multilinestrings: continous, docstring or starting.
371     if s:is_python_string(a:lnum-1, max([1, len(prevline)]))
372                 \ && (s:is_python_string(a:lnum, 1)
373                 \     || match(line, '^\%("""\|''''''\)') != -1)
374
375         " Indent closing quotes as the line with the opening ones.
376         let match_quotes = match(line, '^\s*\zs\%("""\|''''''\)')
377         if match_quotes != -1
378             " closing multiline string
379             let quotes = line[match_quotes:(match_quotes+2)]
380             let pairpos = searchpairpos(quotes, '', quotes, 'b', 1, g:python_pep8_indent_searchpair_timeout)
381             if pairpos[0] != 0
382                 return indent(pairpos[0])
383             else
384                 " TODO: test to cover this!
385             endif
386         endif
387
388         if s:is_python_string(a:lnum-1)
389             " Previous line is (completely) a string: keep current indent.
390             return -1
391         endif
392
393         if match(prevline, '^\s*\%("""\|''''''\)') != -1
394             " docstring.
395             return indent(a:lnum-1)
396         endif
397
398         let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
399                     \ get(g:, 'python_pep8_indent_multiline_string', 0))
400         if match(prevline, '\v%("""|'''''')$') != -1
401             " Opening multiline string, started in previous line.
402             if (&autoindent && indent(a:lnum) == indent(a:lnum-1))
403                         \ || match(line, '\v^\s+$') != -1
404                 " <CR> with empty line or to split up 'foo("""bar' into
405                 " 'foo("""' and 'bar'.
406                 if indent_multi == -2
407                     return indent(a:lnum-1) + s:sw()
408                 endif
409                 return indent_multi
410             endif
411         endif
412
413         " Keep existing indent.
414         if match(line, '\v^\s*\S') != -1
415             return -1
416         endif
417
418         if indent_multi != -2
419             return indent_multi
420         endif
421
422         return s:indent_like_opening_paren(a:lnum)
423     endif
424
425     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
426     let indent = s:indent_like_opening_paren(a:lnum)
427     if indent >= -1
428         return indent
429     endif
430
431     " Blocks: Match indent of first block of this type.
432     let indent = s:indent_like_block(a:lnum)
433     if indent >= -1
434         return indent
435     endif
436
437     return s:indent_like_previous_line(a:lnum)
438 endfunction