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

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