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

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