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

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