" 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
if match( func, 'define-' ) >= 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)