vim/indent/lisp.vim @ a625ea210202

Add jank-ass ripped slimv lisp indenter
author Steve Losh <steve@stevelosh.com>
date Mon, 31 Aug 2015 12:48:03 +0000
parents (none)
children e7642253ffde
" lisp.vim:
"               Lisp indent plugin for Slimv
" Version:      0.9.5
" Last Change:  21 Feb 2012
" Maintainer:   Tamas Kovacs <kovisoft at gmail dot com>
" License:      This file is placed in the public domain.
"               No warranty, express or implied.
"               *** ***   Use At-Your-Own-Risk!   *** ***
"
" =====================================================================
"
"  Load Once:
if exists("b:did_indent")
   finish
endif

" Handle cases when lisp dialects explicitly use the lisp indent plugins
if &ft == "clojure" && exists("g:slimv_disable_clojure")
    finish
endif

if &ft == "scheme" && exists("g:slimv_disable_scheme")
    finish
endif 


" Maximum number of lines searched backwards for indenting special forms
if !exists( 'g:slimv_indent_maxlines' )
    let g:slimv_indent_maxlines = 50
endif

" Special indentation for keyword lists
if !exists( 'g:slimv_indent_keylists' )
    let g:slimv_indent_keylists = 1
endif

let s:skip_sc = 'synIDattr(synID(line("."), col("."), 0), "name") =~ "[Ss]tring\\|[Cc]omment"'
                                                          " Skip matches inside string or comment 
let s:skip_q = 'getline(".")[col(".")-2] == "\\"'         " Skip escaped double quote characters in matches

let s:indent = ''                                         " Most recent indentation info
let s:spec_indent = 'flet\|labels\|macrolet\|symbol-macrolet'
                                                          " List of symbols need special indenting
let s:spec_param = 'defmacro'                             " List of symbols with special parameter list
let s:binding_form = 'let\|let\*'                         " List of symbols with binding list

" Get the filetype (Lisp dialect) used by Slimv
function! SlimvGetFiletype()
    if &ft != ''
        " Return Vim filetype if defined
        return &ft
    endif

    if match( tolower( g:slimv_lisp ), 'clojure' ) >= 0 || match( tolower( g:slimv_lisp ), 'clj' ) >= 0
        " Must be Clojure
        return 'clojure'
    endif

    " We have no clue, guess its lisp
    return 'lisp'
endfunction

" Some multi-byte characters screw up the built-in lispindent()
" This function is a wrapper that tries to fix it
" TODO: implement custom indent procedure and omit lispindent()
function! SlimvLispindent( lnum )
    set lisp
    let li = lispindent( a:lnum )
    set nolisp
    let backline = max([a:lnum-g:slimv_indent_maxlines, 1])
    let oldpos = getpos( '.' )
    call cursor( oldpos[1], 1 )
    " Find containing form
    let [lhead, chead] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
    if lhead == 0
        " No containing form, lispindent() is OK
        call cursor( oldpos[1], oldpos[2] )
        return li
    endif
    " Find outer form
    let [lparent, cparent] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
    call cursor( oldpos[1], oldpos[2] )
    if lparent == 0 || lhead != lparent
        " No outer form or starting above inner form, lispindent() is OK
        return li
    endif
    " Count extra bytes before the function header
    let header = strpart( getline( lparent ), 0 )
    let total_extra = 0
    let extra = 0
    let c = 0
    while a:lnum > 0 && c < chead-1
        let bytes = byteidx( header, c+1 ) - byteidx( header, c )
        if bytes > 1
            let total_extra = total_extra + bytes - 1
            if c >= cparent && extra < 10
                " Extra bytes in the outer function header
                let extra = extra + bytes - 1
            endif
        endif
        let c = c + 1
    endwhile
    if total_extra == 0  
        " No multi-byte character, lispindent() is OK
        return li
    endif
    " In some cases ending spaces add up to lispindent() if there are multi-byte characters
    let ending_sp = len( matchstr( getline( lparent ), ' *$' ) )
    " Determine how wrong lispindent() is based on the number of extra bytes
    " These values were determined empirically
    if lparent == a:lnum - 1
        " Function header is in the previous line
        if extra == 0 && total_extra > 1
            let ending_sp = ending_sp + 1
        endif
        return li + [0, 1, 0, -3, -3, -3, -5, -5, -7, -7, -8][extra] - ending_sp
    else
        " Function header is in an upper line
        if extra == 0 || total_extra == extra
            let ending_sp = 0
        endif
        return li + [0, 1, 0, -2, -2, -3, -3, -3, -3, -3, -3][extra] - ending_sp
    endif
endfunction

" Return Lisp source code indentation at the given line
function! SlimvIndent( lnum )
    if &autoindent == 0 || a:lnum <= 1
        " Start of the file
        return 0
    endif
    let pnum = prevnonblank(a:lnum - 1)
    if pnum == 0
        " Hit the start of the file, use zero indent.
        return 0
    endif
    let oldpos = getpos( '.' )
    let linenum = a:lnum

    " Handle multi-line string
    let plen = len( getline( pnum ) )
    if synIDattr( synID( pnum, plen, 0), 'name' ) =~ '[Ss]tring' && getline(pnum)[plen-1] != '"'
        " Previous non-blank line ends with an unclosed string, so this is a multi-line string
        let [l, c] = searchpairpos( '"', '', '"', 'bnW', s:skip_q )
        if l == pnum && c > 0
            " Indent to the opening double quote (if found)
            return c
        else
            return SlimvLispindent( linenum )
        endif
    endif
    if synIDattr( synID( pnum, 1, 0), 'name' ) =~ '[Ss]tring' && getline(pnum)[0] != '"'
        " Previous non-blank line is the last line of a multi-line string
        call cursor( pnum, 1 )
        " First find the end of the multi-line string (omit \" characters)
        let [lend, cend] = searchpos( '[^\\]"', 'nW' )
        if lend > 0 && strpart(getline(lend), cend+1) =~ '(\|)\|\[\|\]\|{\|}'
            " Structural change after the string, no special handling
        else
            " Find the start of the multi-line string (omit \" characters)
            let [l, c] = searchpairpos( '"', '', '"', 'bnW', s:skip_q )
            if l > 0 && strpart(getline(l), 0, c-1) =~ '^\s*$'
                " Nothing else before the string: indent to the opening "
                return c - 1
            endif
            if l > 0
                " Pretend that we are really after the first line of the multi-line string
                let pnum = l
                let linenum = l + 1
            endif
        endif
        call cursor( oldpos[1], oldpos[2] )
    endif

    " Handle special indentation style for flet, labels, etc.
    " When searching for containing forms, don't go back
    " more than g:slimv_indent_maxlines lines.
    let backline = max([pnum-g:slimv_indent_maxlines, 1])
    let indent_keylists = g:slimv_indent_keylists

    " Check if the previous line actually ends with a multi-line subform
    let parent = pnum
    let [l, c] = searchpos( ')', 'bW' )
    if l == pnum
        let [l, c] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
        if l > 0
            " Make sure it is not a top level form and the containing form starts in the same line
            let [l2, c2] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
            if l2 == l
                " Remember the first line of the multi-line form
                let parent = l
            endif
        endif
    endif

    " Find beginning of the innermost containing form
    call cursor( oldpos[1], 1 )
    let [l, c] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
    if l > 0
        if SlimvGetFiletype() =~ '.*\(clojure\|scheme\|racket\).*'
            " Is this a clojure form with [] binding list?
            call cursor( oldpos[1], oldpos[2] )
            let [lb, cb] = searchpairpos( '\[', '', '\]', 'bW', s:skip_sc, backline )
            if lb >= l && (lb > l || cb > c)
                return cb
            endif
        endif
        " Is this a form with special indentation?
        let line = strpart( getline(l), c-1 )
        if match( line, '\c^(\s*\('.s:spec_indent.'\)\>' ) >= 0
            " Search for the binding list and jump to its end
            if search( '(' ) > 0
                call searchpair( '(', '', ')', '', s:skip_sc )
                if line('.') == pnum
                    " We are indenting the first line after the end of the binding list
                    return c + 1
                endif
            endif
        elseif l == pnum
            " If the containing form starts above this line then find the
            " second outer containing form (possible start of the binding list)
            let [l2, c2] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
            if l2 > 0
                let line2 = strpart( getline(l2), c2-1 )
                if match( line2, '\c^(\s*\('.s:spec_param.'\)\>' ) >= 0
                    if search( '(' ) > 0
                        if line('.') == l && col('.') == c
                            " This is the parameter list of a special form
                            return c
                        endif
                    endif
                endif
                if SlimvGetFiletype() !~ '.*clojure.*'
                    if l2 == l && match( line2, '\c^(\s*\('.s:binding_form.'\)\>' ) >= 0
                        " Is this a lisp form with binding list?
                        return c
                    endif
                    if match( line2, '\c^(\s*cond\>' ) >= 0 && match( line, '\c^(\s*t\>' ) >= 0
                        " Is this the 't' case for a 'cond' form?
                        return c
                    endif
                    if match( line2, '\c^(\s*defpackage\>' ) >= 0
                        let indent_keylists = 0
                    endif
                endif
                " Go one level higher and check if we reached a special form
                let [l3, c3] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
                if l3 > 0
                    " Is this a form with special indentation?
                    let line3 = strpart( getline(l3), c3-1 )
                    if match( line3, '\c^(\s*\('.s:spec_indent.'\)\>' ) >= 0
                        " This is the first body-line of a binding
                        return c + 1
                    endif
                    if match( line3, '\c^(\s*defsystem\>' ) >= 0
                        let indent_keylists = 0
                    endif
                    " Finally go to the topmost level to check for some forms with special keyword indenting
                    let [l4, c4] = searchpairpos( '(', '', ')', 'brW', s:skip_sc, backline )
                    if l4 > 0
                        let line4 = strpart( getline(l4), c4-1 )
                        if match( line4, '\c^(\s*defsystem\>' ) >= 0
                            let indent_keylists = 0
                        endif
                    endif
                endif
            endif
        endif
    endif

    " Restore all cursor movements
    call cursor( oldpos[1], oldpos[2] )

    " Check if the current form started in the previous nonblank line
    if l == parent
        " Found opening paren in the previous line
        let line = getline(l)
        let form = strpart( line, c )
        " Determine the length of the function part up to the 1st argument
        let funclen = matchend( form, '\s*\S*\s*' ) + 1
        " Contract strings, remove comments
        let form = substitute( form, '".\{-}[^\\]"', '""', 'g' )
        let form = substitute( form, ';.*$', '', 'g' )
        " Contract subforms by replacing them with a single character
        let f = ''
        while form != f
            let f = form
            let form = substitute( form, '([^()]*)',     '0', 'g' )
            let form = substitute( form, '([^()]*$',     '0', 'g' )
            let form = substitute( form, '\[[^\[\]]*\]', '0', 'g' )
            let form = substitute( form, '\[[^\[\]]*$',  '0', 'g' )
            let form = substitute( form, '{[^{}]*}',     '0', 'g' )
            let form = substitute( form, '{[^{}]*$',     '0', 'g' )
        endwhile
        " Find out the function name
        let func = matchstr( form, '\<\k*\>' )
        " If it's a keyword, keep the indentation straight
        if indent_keylists && strpart(func, 0, 1) == ':'
            if form =~ '^:\S*\s\+\S'
                " This keyword has an associated value in the same line
                return c
            else
                " The keyword stands alone in its line with no associated value
                return c + 1
            endif
        endif
        if SlimvGetFiletype() =~ '.*clojure.*'
            " Fix clojure specific indentation issues not handled by the default lisp.vim
            if match( func, 'defn$' ) >= 0
                return c + 1
            endif
        else
            if match( func, 'defgeneric$' ) >= 0 || match( func, 'defsystem$' ) >= 0 || match( func, 'aif$' ) >= 0
                return c + 1
            endif
        endif
        " Remove package specification
        let func = substitute(func, '^.*:', '', '')
        if func != '' && s:swank_connected
            " Look how many arguments are on the same line
            " If an argument is actually a multi-line subform, then replace it with a single character
            let form = substitute( form, "([^()]*$", '0', 'g' )
            let form = substitute( form, "[()\\[\\]{}#'`,]", '', 'g' )
            let args_here = len( split( form ) ) - 1
            " Get swank indent info
            let s:indent = ''
            silent execute 'python get_indent_info("' . func . '")'
            if s:indent != '' && s:indent == args_here
                " The next one is an &body argument, so indent by 2 spaces from the opening '('
                return c + 1
            endif
            let llen = len( line )
            if synIDattr( synID( l, llen, 0), 'name' ) =~ '[Ss]tring' && line[llen-1] != '"'
                " Parent line ends with a multi-line string
                " lispindent() fails to handle it correctly
                if s:indent == '' && args_here > 0
                    " No &body argument, ignore lispindent() and indent to the 1st argument
                    return c + funclen - 1
                endif
            endif
        endif
    endif

    " Use default Lisp indenting
    let li = SlimvLispindent(linenum)
    let line = strpart( getline(linenum-1), li-1 )
    let gap = matchend( line, '^(\s\+\S' )
    if gap >= 0
        " Align to the gap between the opening paren and the first atom
        return li + gap - 2
    endif
    return li
endfunction 

" Convert indent value to spaces or a mix of tabs and spaces
" depending on the value of 'expandtab'
function! s:MakeIndent( indent )
    if &expandtab
        return repeat( ' ', a:indent )
    else
        return repeat( "\<Tab>", a:indent / &tabstop ) . repeat( ' ', a:indent % &tabstop )
    endif
endfunction


setlocal nolisp
setlocal autoindent
setlocal expandtab
setlocal indentexpr=SlimvIndent(v:lnum)