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

Merge pull request #91 from Vimjas/maxoff-by-type
[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 let s:block_rules = {
38             \ '^\s*elif\>': ['if', 'elif'],
39             \ '^\s*except\>': ['try', 'except'],
40             \ '^\s*finally\>': ['try', 'except', 'else']
41             \ }
42 let s:block_rules_multiple = {
43             \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
44             \ }
45 " Pairs to look for when searching for opening parenthesis.
46 " The value is the maximum offset in lines.
47 let s:paren_pairs = {'()': 50, '[]': 100, '{}': 1000}
48
49 " Maximum offset when looking for multiline statements (in round parenthesis).
50 let s:maxoff_multiline_statement = 50
51
52 if &filetype ==# 'pyrex' || &filetype ==# 'cython'
53     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
54 else
55     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
56 endif
57 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
58
59 " Skip strings and comments. Return 1 for chars to skip.
60 " jedi* refers to syntax definitions from jedi-vim for call signatures, which
61 " are inserted temporarily into the buffer.
62 let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
63             \ '=~? "\\vstring|comment|jedi\\S"'
64
65 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
66             \ '=~? "\\vcomment|jedi\\S"'
67
68 " Also ignore anything concealed.
69 " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
70 function! s:is_concealed(line, col)
71     let concealed = synconcealed(a:line, a:col)
72     return len(concealed) && concealed[0]
73 endfunction
74 if has('conceal')
75     let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
76 endif
77
78
79 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
80             \ '=~? "comment"'
81
82 " Use 'shiftwidth()' instead of '&sw'.
83 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
84 if exists('*shiftwidth')
85     function! s:sw()
86         return shiftwidth()
87     endfunction
88 else
89     function! s:sw()
90         return &shiftwidth
91     endfunction
92 endif
93
94 function! s:pair_sort(x, y)
95     if a:x[0] == a:y[0]
96         return a:x[1] == a:y[1] ? 0 : a:x[1] > a:y[1] ? 1 : -1
97     else
98         return a:x[0] > a:y[0] ? 1 : -1
99     endif
100 endfunction
101
102 " Find backwards the closest open parenthesis/bracket/brace.
103 function! s:find_opening_paren(...)
104     " optional arguments: line and column (defaults to 1) to search around
105     if a:0 > 0
106         let view = winsaveview()
107         call cursor(a:1, a:0 > 1 ? a:2 : 1)
108         let ret = s:find_opening_paren()
109         call winrestview(view)
110         return ret
111     endif
112
113     " Return if cursor is in a comment.
114     exe 'if' s:skip_search '| return [0, 0] | endif'
115
116     let nearest = [0, 0]
117     for [p, maxoff] in items(s:paren_pairs)
118         let stopline = max([0, line('.') - maxoff, nearest[0]])
119         let next = searchpairpos(
120            \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline)
121         if next[0] && (next[0] > nearest[0] || (next[0] == nearest[0] && next[1] > nearest[1]))
122             let nearest = next
123         endif
124     endfor
125     return nearest
126 endfunction
127
128 " Find the start of a multi-line statement (based on surrounding parens).
129 function! s:find_start_of_multiline_statement(lnum)
130     let lnum = a:lnum
131     while lnum > 0
132         " XXX: not tested?!
133         if getline(lnum - 1) =~# '\\$'
134             let lnum = prevnonblank(lnum - 1)
135         else
136             call cursor(lnum, 1)
137             let stopline = max([1, lnum - s:maxoff_multiline_statement])
138             let pos = searchpairpos('\V(', '', '\V)', 'bnW', s:skip_special_chars, stopline)
139             if pos[0]
140                 return pos[0]
141             endif
142             return lnum
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     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     let linelen = len(line)
344     if linelen < 1
345       let linelen = 1
346     endif
347     let cols = a:0 ? type(a:1) != type([]) ? [a:1] : a:1 : range(1, linelen)
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, 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')
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.
386             return indent(a:lnum-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