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

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