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

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