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

Indent empty lines inside blocks (#81)
[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 expandtab
29 setlocal nolisp
30 setlocal autoindent
31 setlocal indentexpr=GetPythonPEPIndent(v:lnum)
32 setlocal indentkeys=!^F,o,O,<:>,0),0],0},=elif,=except
33 setlocal tabstop=4
34 setlocal softtabstop=4
35 setlocal shiftwidth=4
36
37 if !exists('g:python_pep8_indent_multiline_string')
38     let g:python_pep8_indent_multiline_string = 0
39 endif
40
41 let s:maxoff = 50
42 let s:block_rules = {
43             \ '^\s*elif\>': ['if', 'elif'],
44             \ '^\s*except\>': ['try', 'except'],
45             \ '^\s*finally\>': ['try', 'except', 'else']
46             \ }
47 let s:block_rules_multiple = {
48             \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
49             \ }
50 let s:paren_pairs = ['()', '{}', '[]']
51 if &ft == 'pyrex' || &ft == 'cython'
52     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except|cdef|cpdef)>'
53 else
54     let b:control_statement = '\v^\s*(class|def|if|while|with|for|except)>'
55 endif
56 let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
57
58 " Skip strings and comments. Return 1 for chars to skip.
59 " jedi* refers to syntax definitions from jedi-vim for call signatures, which
60 " are inserted temporarily into the buffer.
61 let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
62             \ '=~? "\\vstring|comment|jedi\\S"'
63
64 let s:skip_after_opening_paren = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
65             \ '=~? "\\vcomment|jedi\\S"'
66
67 " Also ignore anything concealed.
68 " Wrapper around synconcealed for older Vim (7.3.429, used on Travis CI).
69 function! s:is_concealed(line, col)
70     let concealed = synconcealed(a:line, a:col)
71     return len(concealed) && concealed[0]
72 endfunction
73 if has('conceal')
74     let s:skip_special_chars .= '|| s:is_concealed(line("."), col("."))'
75 endif
76
77
78 let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
79             \ '=~? "comment"'
80
81 " Use 'shiftwidth()' instead of '&sw'.
82 " (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
83 if exists('*shiftwidth')
84     function! s:sw()
85         return shiftwidth()
86     endfunction
87 else
88     function! s:sw()
89         return &sw
90     endfunction
91 endif
92
93 function! s:pair_sort(x, y)
94     if a:x[0] == a:y[0]
95         return a:x[1] == a:y[1] ? 0 : a:x[1] > a:y[1] ? 1 : -1
96     else
97         return a:x[0] > a:y[0] ? 1 : -1
98     endif
99 endfunction
100
101 " Find backwards the closest open parenthesis/bracket/brace.
102 function! s:find_opening_paren(...)
103     " optional arguments: line and column (defaults to 1) to search around
104     if a:0 > 0
105         let view = winsaveview()
106         call cursor(a:1, a:0 > 1 ? a:2 : 1)
107         let ret = s:find_opening_paren()
108         call winrestview(view)
109         return ret
110     endif
111
112     let stopline = max([0, line('.') - s:maxoff])
113
114     " Return if cursor is in a comment.
115     exe 'if' s:skip_search '| return [0, 0] | endif'
116
117     let positions = []
118     for p in s:paren_pairs
119         call add(positions, searchpairpos(
120            \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline))
121     endfor
122
123     " Remove empty matches and return the type with the closest match
124     call filter(positions, 'v:val[0]')
125     call sort(positions, 's:pair_sort')
126
127     return get(positions, -1, [0, 0])
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 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     " If the previous statement was a stop-execution statement or a pass
309     if getline(start) =~# s:stop_statement
310         " Remove one level of indentation if the user hasn't already dedented
311         if empty || current > base - s:sw()
312             return base - s:sw()
313         endif
314         " Otherwise, trust the user
315         return -1
316     endif
317
318     if !empty && s:is_dedented_already(current, base)
319         return -1
320     endif
321
322     " In all other cases, line up with the start of the previous statement.
323     return base
324 endfunction
325
326 " If this line is dedented and the number of indent spaces is valid
327 " (multiple of the indentation size), trust the user.
328 function! s:is_dedented_already(current, base)
329     let dedent_size = a:current - a:base
330     return (dedent_size < 0 && a:current % s:sw() == 0) ? 1 : 0
331 endfunction
332
333 " Is the syntax at lnum (and optionally cnum) a python string?
334 function! s:is_python_string(lnum, ...)
335     let line = getline(a:lnum)
336     let linelen = len(line)
337     if linelen < 1
338       let linelen = 1
339     endif
340     let cols = a:0 ? type(a:1) != type([]) ? [a:1] : a:1 : range(1, linelen)
341     for cnum in cols
342         if match(map(synstack(a:lnum, cnum),
343                     \ 'synIDattr(v:val,"name")'), 'python\S*String') == -1
344             return 0
345         end
346     endfor
347     return 1
348 endfunction
349
350 function! GetPythonPEPIndent(lnum)
351     " First line has indent 0
352     if a:lnum == 1
353         return 0
354     endif
355
356     let line = getline(a:lnum)
357     let prevline = getline(a:lnum-1)
358
359     " Multilinestrings: continous, docstring or starting.
360     if s:is_python_string(a:lnum-1, len(prevline))
361                 \ && (s:is_python_string(a:lnum, 1)
362                 \     || match(line, '^\%("""\|''''''\)') != -1)
363
364         " Indent closing quotes as the line with the opening ones.
365         let match_quotes = match(line, '^\s*\zs\%("""\|''''''\)')
366         if match_quotes != -1
367             " closing multiline string
368             let quotes = line[match_quotes:match_quotes+2]
369             let pairpos = searchpairpos(quotes, '', quotes, 'b')
370             if pairpos[0] != 0
371                 return indent(pairpos[0])
372             else
373                 " TODO: test to cover this!
374             endif
375         endif
376
377         if s:is_python_string(a:lnum-1)
378             " Previous line is (completely) a string.
379             return indent(a:lnum-1)
380         endif
381
382         if match(prevline, '^\s*\%("""\|''''''\)') != -1
383             " docstring.
384             return indent(a:lnum-1)
385         endif
386
387         let indent_multi = get(b:, 'python_pep8_indent_multiline_string',
388                     \ get(g:, 'python_pep8_indent_multiline_string', 0))
389         if match(prevline, '\v%("""|'''''')$') != -1
390             " Opening multiline string, started in previous line.
391             if (&autoindent && indent(a:lnum) == indent(a:lnum-1))
392                         \ || match(line, '\v^\s+$') != -1
393                 " <CR> with empty line or to split up 'foo("""bar' into
394                 " 'foo("""' and 'bar'.
395                 if indent_multi == -2
396                     return indent(a:lnum-1) + s:sw()
397                 endif
398                 return indent_multi
399             endif
400         endif
401
402         " Keep existing indent.
403         if match(line, '\v^\s*\S') != -1
404             return -1
405         endif
406
407         if indent_multi != -2
408             return indent_multi
409         endif
410
411         return s:indent_like_opening_paren(a:lnum)
412     endif
413
414     " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
415     let indent = s:indent_like_opening_paren(a:lnum)
416     if indent >= -1
417         return indent
418     endif
419
420     " Blocks: Match indent of first block of this type.
421     let indent = s:indent_like_block(a:lnum)
422     if indent >= -1
423         return indent
424     endif
425
426     return s:indent_like_previous_line(a:lnum)
427 endfunction