]> git.madduck.net Git - etc/vim.git/blobdiff - 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:

Merge pull request #42 from blueyed/more-indent-function-def
[etc/vim.git] / indent / python.vim
index 1dabb475c043533578dc6eebcee53d4d288f753e..007c7e160ea0810803a8086de4f289fe497c0b00 100644 (file)
@@ -3,8 +3,19 @@
 " Maintainer:       Hynek Schlawack <hs@ox.cx>
 " Prev Maintainer:  Eric Mc Sween <em@tomcom.de> (address invalid)
 " Original Author:  David Bustos <bustos@caltech.edu> (address invalid)
 " Maintainer:       Hynek Schlawack <hs@ox.cx>
 " Prev Maintainer:  Eric Mc Sween <em@tomcom.de> (address invalid)
 " Original Author:  David Bustos <bustos@caltech.edu> (address invalid)
-" Last Change:      2012-06-21
-" License:          Public Domain
+" License:          CC0
+"
+" vim-python-pep8-indent - A nicer Python indentation style for vim.
+" Written in 2004 by David Bustos <bustos@caltech.edu>
+" Maintained from 2004-2005 by Eric Mc Sween <em@tomcom.de>
+" Maintained from 2013 by Hynek Schlawack <hs@ox.cx>
+"
+" To the extent possible under law, the author(s) have dedicated all copyright
+" and related and neighboring rights to this software to the public domain
+" worldwide. This software is distributed without any warranty.
+" You should have received a copy of the CC0 Public Domain Dedication along
+" with this software. If not, see
+" <http://creativecommons.org/publicdomain/zero/1.0/>.
 
 " Only load this indent file when no other was loaded.
 if exists("b:did_indent")
 
 " Only load this indent file when no other was loaded.
 if exists("b:did_indent")
@@ -15,180 +26,279 @@ let b:did_indent = 1
 setlocal expandtab
 setlocal nolisp
 setlocal autoindent
 setlocal expandtab
 setlocal nolisp
 setlocal autoindent
-setlocal indentexpr=GetPythonIndent(v:lnum)
+setlocal indentexpr=GetPythonPEPIndent(v:lnum)
 setlocal indentkeys=!^F,o,O,<:>,0),0],0},=elif,=except
 setlocal indentkeys=!^F,o,O,<:>,0),0],0},=elif,=except
+setlocal tabstop=4
+setlocal softtabstop=4
+setlocal shiftwidth=4
 
 let s:maxoff = 50
 
 let s:maxoff = 50
+let s:block_rules = {
+            \ '^\s*elif\>': ['if', 'elif'],
+            \ '^\s*else\>': ['if', 'elif', 'for', 'try', 'except'],
+            \ '^\s*except\>': ['try', 'except'],
+            \ '^\s*finally\>': ['try', 'except', 'else']
+            \ }
+let s:paren_pairs = ['()', '{}', '[]']
+let s:control_statement = '^\s*\(class\|def\|if\|while\|with\|for\|except\)\>'
+let s:stop_statement = '^\s*\(break\|continue\|raise\|return\|pass\)\>'
 
 
-" Find backwards the closest open parenthesis/bracket/brace.
-function! s:SearchParensPair()
-    let line = line('.')
-    let col = col('.')
-
-    " Skip strings and comments and don't look too far
-    let skip = "line('.') < " . (line - s:maxoff) . " ? dummy :" .
-                \ 'synIDattr(synID(line("."), col("."), 0), "name") =~? ' .
-                \ '"string\\|comment"'
-
-    " Search for parentheses
-    call cursor(line, col)
-    let parlnum = searchpair('(', '', ')', 'bW', skip)
-    let parcol = col('.')
-
-    " Search for brackets
-    call cursor(line, col)
-    let par2lnum = searchpair('\[', '', '\]', 'bW', skip)
-    let par2col = col('.')
-
-    " Search for braces
-    call cursor(line, col)
-    let par3lnum = searchpair('{', '', '}', 'bW', skip)
-    let par3col = col('.')
-
-    " Get the closest match
-    if par2lnum > parlnum || (par2lnum == parlnum && par2col > parcol)
-        let parlnum = par2lnum
-        let parcol = par2col
-    endif
-    if par3lnum > parlnum || (par3lnum == parlnum && par3col > parcol)
-        let parlnum = par3lnum
-        let parcol = par3col
+" Skip strings and comments
+let s:skip_special_chars = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
+            \ '=~? "string\\|comment"'
+
+let s:skip_search = 'synIDattr(synID(line("."), col("."), 0), "name") ' .
+            \ '=~? "comment"'
+
+" Use 'shiftwidth()' instead of '&sw'.
+" (Since Vim patch 7.3.629, 'shiftwidth' can be set to 0 to follow 'tabstop').
+if exists('*shiftwidth')
+    function! s:sw()
+        return shiftwidth()
+    endfunction
+else
+    function! s:sw()
+        return &sw
+    endfunction
+endif
+
+function! s:pair_sort(x, y)
+    if a:x[0] == a:y[0]
+        return a:x[1] == a:y[1] ? 0 : a:x[1] > a:y[1] ? 1 : -1
+    else
+        return a:x[0] > a:y[0] ? 1 : -1
     endif
     endif
+endfunction
 
 
-    " Put the cursor on the match
-    if parlnum > 0
-        call cursor(parlnum, parcol)
+" Find backwards the closest open parenthesis/bracket/brace.
+function! s:find_opening_paren(...)
+    " optional arguments: line and column (defaults to 1) to search around
+    if a:0 > 0
+        let view = winsaveview()
+        call cursor(a:1, a:0 > 1 ? a:2 : 1)
+        let ret = s:find_opening_paren()
+        call winrestview(view)
+        return ret
     endif
     endif
-    return parlnum
+
+    let stopline = max([0, line('.') - s:maxoff])
+
+    " Return if cursor is in a comment.
+    exe 'if' s:skip_search '| return [0, 0] | endif'
+
+    let positions = []
+    for p in s:paren_pairs
+        call add(positions, searchpairpos(
+           \ '\V'.p[0], '', '\V'.p[1], 'bnW', s:skip_special_chars, stopline))
+    endfor
+
+    " Remove empty matches and return the type with the closest match
+    call filter(positions, 'v:val[0]')
+    call sort(positions, 's:pair_sort')
+
+    return get(positions, -1, [0, 0])
 endfunction
 
 " Find the start of a multi-line statement
 endfunction
 
 " Find the start of a multi-line statement
-function! s:StatementStart(lnum)
+function! s:find_start_of_multiline_statement(lnum)
     let lnum = a:lnum
     let lnum = a:lnum
-    while 1
+    while lnum > 0
         if getline(lnum - 1) =~ '\\$'
         if getline(lnum - 1) =~ '\\$'
-            let lnum = lnum - 1
+            let lnum = prevnonblank(lnum - 1)
         else
         else
-            call cursor(lnum, 1)
-            let maybe_lnum = s:SearchParensPair()
-            if maybe_lnum < 1
+            let [paren_lnum, _] = s:find_opening_paren(lnum)
+            if paren_lnum < 1
                 return lnum
             else
                 return lnum
             else
-                let lnum = maybe_lnum
+                let lnum = paren_lnum
             endif
         endif
     endwhile
 endfunction
 
 " Find the block starter that matches the current line
             endif
         endif
     endwhile
 endfunction
 
 " Find the block starter that matches the current line
-function! s:BlockStarter(lnum, block_start_re)
+function! s:find_start_of_block(lnum, types)
+    let re = '\V\^\s\*\('.join(a:types, '\|').'\)\>'
+
     let lnum = a:lnum
     let lnum = a:lnum
-    let maxindent = 10000       " whatever
-    while lnum > 1
-        let lnum = prevnonblank(lnum - 1)
-        if indent(lnum) < maxindent
-            if getline(lnum) =~ a:block_start_re
+    let last_indent = indent(lnum) + 1
+    while lnum > 0 && last_indent > 0
+        if indent(lnum) < last_indent
+            if getline(lnum) =~# re
                 return lnum
                 return lnum
-            else
-                let maxindent = indent(lnum)
-                " It's not worth going further if we reached the top level
-                if maxindent == 0
-                    return -1
-                endif
             endif
             endif
+            let last_indent = indent(lnum)
         endif
         endif
+        let lnum = prevnonblank(lnum - 1)
     endwhile
     endwhile
-    return -1
+    return 0
 endfunction
 
 endfunction
 
-function! GetPythonIndent(lnum)
-
-    " First line has indent 0
-    if a:lnum == 1
-        return 0
+" Line up with open parenthesis/bracket/brace.
+function! s:indent_like_opening_paren(lnum)
+    let [paren_lnum, paren_col] = s:find_opening_paren(a:lnum)
+    if paren_lnum <= 0
+        return -2
     endif
     endif
+    let text = getline(paren_lnum)
+    let base = indent(paren_lnum)
 
 
-    " If we can find an open parenthesis/bracket/brace, line up with it.
-    call cursor(a:lnum, 1)
-    let parlnum = s:SearchParensPair()
-    if parlnum > 0
-        let parcol = col('.')
-        let closing_paren = match(getline(a:lnum), '^\s*[])}]') != -1
-        if match(getline(parlnum), '[([{]\s*$', parcol - 1) != -1
-            if closing_paren
-                return indent(parlnum)
-            else
-                return indent(parlnum) + &shiftwidth
-            endif
+    let nothing_after_opening_paren = text =~ '\%'.(paren_col + 1).'c\s*$'
+    let starts_with_closing_paren = getline(a:lnum) =~ '^\s*[])}]'
+
+    if nothing_after_opening_paren
+        if starts_with_closing_paren
+            let res = base
         else
         else
-            return parcol
+            let res = base + s:sw()
         endif
         endif
+    else
+        " Indent to match position of opening paren.
+        let res = paren_col
+    endif
+
+    " If this line is the continuation of a control statement
+    " indent further to distinguish the continuation line
+    " from the next logical line.
+    if text =~# s:control_statement && res == base + s:sw()
+        return base + s:sw() * 2
+    else
+        return res
     endif
     endif
+endfunction
 
 
-    " Examine this line
-    let thisline = getline(a:lnum)
-    let thisindent = indent(a:lnum)
+" Match indent of first block of this type.
+function! s:indent_like_block(lnum)
+    let text = getline(a:lnum)
 
 
-    " If the line starts with 'elif' or 'else', line up with 'if' or 'elif'
-    if thisline =~ '^\s*\(elif\|else\)\>'
-        let bslnum = s:BlockStarter(a:lnum, '^\s*\(if\|elif\)\>')
-        if bslnum > 0
-            return indent(bslnum)
-        else
-            return -1
+    for [line_re, blocks] in items(s:block_rules)
+        if text !~# line_re
+            continue
         endif
         endif
-    endif
 
 
-    " If the line starts with 'except' or 'finally', line up with 'try'
-    " or 'except'
-    if thisline =~ '^\s*\(except\|finally\)\>'
-        let bslnum = s:BlockStarter(a:lnum, '^\s*\(try\|except\)\>')
-        if bslnum > 0
-            return indent(bslnum)
+        let lnum = s:find_start_of_block(a:lnum - 1, blocks)
+        if lnum > 0
+            return indent(lnum)
         else
             return -1
         endif
         else
             return -1
         endif
-    endif
+    endfor
 
 
-    " Examine previous line
-    let plnum = a:lnum - 1
-    let pline = getline(plnum)
-    let sslnum = s:StatementStart(plnum)
+    return -2
+endfunction
 
 
-    " If the previous line is blank, keep the same indentation
-    if pline =~ '^\s*$'
-        return -1
+function! s:indent_like_previous_line(lnum)
+    let lnum = prevnonblank(a:lnum - 1)
+
+    " No previous line, keep current indent.
+    if lnum < 1
+      return -1
     endif
 
     endif
 
-    " If this line is explicitly joined, try to find an indentation that looks
-    " good.
-    if pline =~ '\\$'
-        let compound_statement = '^\s*\(if\|while\|for\s.*\sin\|except\)\s*'
-        let maybe_indent = matchend(getline(sslnum), compound_statement)
-        if maybe_indent != -1
-            return maybe_indent
-        else
-            return indent(sslnum) + &sw * 2
+    let text = getline(lnum)
+    let start = s:find_start_of_multiline_statement(lnum)
+    let base = indent(start)
+    let current = indent(a:lnum)
+
+    " Jump to last character in previous line.
+    call cursor(lnum, len(text))
+    let ignore_last_char = eval(s:skip_special_chars)
+
+    " Search for final colon that is not inside a string or comment.
+    while search(':\s*\%(#.*\)\?$', 'bcW', lnum)
+      if eval(s:skip_special_chars)
+        normal! h
+      else
+        return base + s:sw()
+      endif
+    endwhile
+
+    if text =~ '\\$' && !ignore_last_char
+        " If this line is the continuation of a control statement
+        " indent further to distinguish the continuation line
+        " from the next logical line.
+        if getline(start) =~# s:control_statement
+            return base + s:sw() * 2
         endif
         endif
-    endif
 
 
-    " If the previous line ended with a colon and is not a comment, indent
-    " relative to statement start.
-    if pline =~ '^[^#]*:\s*\(#.*\)\?$'
-        return indent(sslnum) + &sw
+        " Nest (other) explicit continuations only one level deeper.
+        return base + s:sw()
     endif
 
     endif
 
-    " If the previous line was a stop-execution statement or a pass
-    if getline(sslnum) =~ '^\s*\(break\|continue\|raise\|return\|pass\)\>'
-        " See if the user has already dedented
-        if indent(a:lnum) > indent(sslnum) - &sw
-            " If not, recommend one dedent
-            return indent(sslnum) - &sw
+    " If the previous statement was a stop-execution statement or a pass
+    if getline(start) =~# s:stop_statement
+        " Remove one level of indentation if the user hasn't already dedented
+        if indent(a:lnum) > base - s:sw()
+            return base - s:sw()
         endif
         " Otherwise, trust the user
         return -1
     endif
 
         endif
         " Otherwise, trust the user
         return -1
     endif
 
+    " If this line is dedented and the number of indent spaces is valid
+    " (multiple of the indentation size), trust the user
+    let dedent_size = current - base
+    if dedent_size < 0 && current % s:sw() == 0
+        return -1
+    endif
+
     " In all other cases, line up with the start of the previous statement.
     " In all other cases, line up with the start of the previous statement.
-    return indent(sslnum)
+    return base
+endfunction
+
+" Is the syntax at lnum (and optionally cnum) a python string?
+function! s:is_python_string(lnum, ...)
+    let line = getline(a:lnum)
+    let linelen = len(line)
+    if linelen < 1
+      let linelen = 1
+    endif
+    let cols = a:0 ? type(a:1) != type([]) ? [a:1] : a:1 : range(1, linelen)
+    for cnum in cols
+        if match(map(synstack(a:lnum, cnum),
+                    \ 'synIDattr(v:val,"name")'), 'python\S*String') == -1
+            return 0
+        end
+    endfor
+    return 1
+endfunction
+
+function! GetPythonPEPIndent(lnum)
+
+    " First line has indent 0
+    if a:lnum == 1
+        return 0
+    endif
+
+    " Multilinestrings: continous, docstring or starting.
+    if s:is_python_string(a:lnum)
+        if s:is_python_string(a:lnum-1)
+            " Previous line is (completely) a string.
+            return s:indent_like_previous_line(a:lnum)
+        endif
+
+        if match(getline(a:lnum-1), '^\s*\%("""\|''''''\)') != -1
+            " docstring.
+            return s:indent_like_previous_line(a:lnum)
+        endif
+
+        if s:is_python_string(a:lnum-1, len(getline(a:lnum-1)))
+            " String started in previous line.
+            return 0
+        endif
+    endif
+
+    " Parens: If we can find an open parenthesis/bracket/brace, line up with it.
+    let indent = s:indent_like_opening_paren(a:lnum)
+    if indent >= -1
+        return indent
+    endif
+
+    " Blocks: Match indent of first block of this type.
+    let indent = s:indent_like_block(a:lnum)
+    if indent >= -1
+        return indent
+    endif
+
+    return s:indent_like_previous_line(a:lnum)
 endfunction
 endfunction