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

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