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