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