" Vim indent file
" Language:     Clojure
" Author:       Meikel Brandmeyer <mb@kotka.de>
" URL:          http://kotka.de/projects/clojure/vimclojure.html
"
" Maintainer:   Sung Pae <self@sungpae.com>
" URL:          https://github.com/guns/vim-clojure-static
" License:      Same as Vim
" Last Change:  30 January 2013

" Only load this indent file when no other was loaded.
if exists("b:did_indent")
    finish
endif
let b:did_indent = 1

let s:save_cpo = &cpo
set cpo&vim

let b:undo_indent = 'setlocal autoindent< smartindent< lispwords< expandtab< softtabstop< shiftwidth< indentexpr< indentkeys<'

setlocal noautoindent nosmartindent
setlocal softtabstop=2 shiftwidth=2 expandtab
setlocal indentkeys=!,o,O

if exists("*searchpairpos")

    if !exists('g:clojure_maxlines')
        let g:clojure_maxlines = 100
    endif

    if !exists('g:clojure_fuzzy_indent')
        let g:clojure_fuzzy_indent = 1
    endif

    if !exists('g:clojure_fuzzy_indent_patterns')
        let g:clojure_fuzzy_indent_patterns = ['^with', '^def', '^let']
    endif

    if !exists('g:clojure_fuzzy_indent_blacklist')
        let g:clojure_fuzzy_indent_blacklist = ['-fn$', '\v^with-%(meta|out-str|loading-context)$']
    endif

    if !exists('g:clojure_special_indent_words')
        let g:clojure_special_indent_words = 'deftype,defrecord,reify,proxy,extend-type,extend-protocol,letfn'
    endif

    if !exists('g:clojure_align_multiline_strings')
        let g:clojure_align_multiline_strings = 0
    endif

    function! s:SynIdName()
        return synIDattr(synID(line("."), col("."), 0), "name")
    endfunction

    function! s:CurrentChar()
        return getline('.')[col('.')-1]
    endfunction

    function! s:CurrentWord()
        return getline('.')[col('.')-1 : searchpos('\v>', 'n', line('.'))[1]-2]
    endfunction

    function! s:IsParen()
        return s:CurrentChar() =~ '\v[\(\)\[\]\{\}]' &&
             \ s:SynIdName() !~? '\vstring|comment'
    endfunction

    " Returns 1 if string matches a pattern in 'patterns', which may be a
    " list of patterns, or a comma-delimited string of implicitly anchored
    " patterns.
    function! s:MatchesOne(patterns, string)
        let list = type(a:patterns) == type([])
                   \ ? a:patterns
                   \ : map(split(a:patterns, ','), '"^" . v:val . "$"')
        for pat in list
            if a:string =~ pat | return 1 | endif
        endfor
    endfunction

    function! s:SavePosition()
        let [ _b, l, c, _o ] = getpos(".")
        let b = bufnr("%")
        return [b, l, c]
    endfunction

    function! s:RestorePosition(value)
        let [b, l, c] = a:value
        if bufnr("%") != b
            execute b "buffer!"
        endif
        call setpos(".", [0, l, c, 0])
    endfunction

    function! s:MatchPairs(open, close, stopat)
        " Stop only on vector and map [ resp. {. Ignore the ones in strings and
        " comments.
        if a:stopat == 0
            let stopat = max([line(".") - g:clojure_maxlines, 0])
        else
            let stopat = a:stopat
        endif

        let pos = searchpairpos(a:open, '', a:close, 'bWn', "!s:IsParen()", stopat)
        return [pos[0], virtcol(pos)]
    endfunction

    function! s:ClojureCheckForStringWorker()
        " Check whether there is the last character of the previous line is
        " highlighted as a string. If so, we check whether it's a ". In this
        " case we have to check also the previous character. The " might be the
        " closing one. In case the we are still in the string, we search for the
        " opening ". If this is not found we take the indent of the line.
        let nb = prevnonblank(v:lnum - 1)

        if nb == 0
            return -1
        endif

        call cursor(nb, 0)
        call cursor(0, col("$") - 1)
        if s:SynIdName() !~? "string"
            return -1
        endif

        " This will not work for a " in the first column...
        if s:CurrentChar() == '"'
            call cursor(0, col("$") - 2)
            if s:SynIdName() !~? "string"
                return -1
            endif
            if s:CurrentChar() != '\\'
                return -1
            endif
            call cursor(0, col("$") - 1)
        endif

        let p = searchpos('\(^\|[^\\]\)\zs"', 'bW')

        if p != [0, 0]
            return p[1] - 1
        endif

        return indent(".")
    endfunction

    function! s:CheckForString()
        let pos = s:SavePosition()
        try
            let val = s:ClojureCheckForStringWorker()
        finally
            call s:RestorePosition(pos)
        endtry
        return val
    endfunction

    function! s:ClojureIsMethodSpecialCaseWorker(position)
        " Find the next enclosing form.
        call search('\S', 'Wb')

        " Special case: we are at a '(('.
        if s:CurrentChar() == '('
            return 0
        endif
        call cursor(a:position)

        let nextParen = s:MatchPairs('(', ')', 0)

        " Special case: we are now at toplevel.
        if nextParen == [0, 0]
            return 0
        endif
        call cursor(nextParen)

        call search('\S', 'W')
        if g:clojure_special_indent_words =~ '\<' . s:CurrentWord() . '\>'
            return 1
        endif

        return 0
    endfunction

    function! s:IsMethodSpecialCase(position)
        let pos = s:SavePosition()
        try
            let val = s:ClojureIsMethodSpecialCaseWorker(a:position)
        finally
            call s:RestorePosition(pos)
        endtry
        return val
    endfunction

    function! GetClojureIndent()
        " Get rid of special case.
        if line(".") == 1
            return 0
        endif

        " We have to apply some heuristics here to figure out, whether to use
        " normal lisp indenting or not.
        let i = s:CheckForString()
        if i > -1
            return i + !!g:clojure_align_multiline_strings
        endif

        call cursor(0, 1)

        " Find the next enclosing [ or {. We can limit the second search
        " to the line, where the [ was found. If no [ was there this is
        " zero and we search for an enclosing {.
        let paren = s:MatchPairs('(', ')', 0)
        let bracket = s:MatchPairs('\[', '\]', paren[0])
        let curly = s:MatchPairs('{', '}', bracket[0])

        " In case the curly brace is on a line later then the [ or - in
        " case they are on the same line - in a higher column, we take the
        " curly indent.
        if curly[0] > bracket[0] || curly[1] > bracket[1]
            if curly[0] > paren[0] || curly[1] > paren[1]
                return curly[1]
            endif
        endif

        " If the curly was not chosen, we take the bracket indent - if
        " there was one.
        if bracket[0] > paren[0] || bracket[1] > paren[1]
            return bracket[1]
        endif

        " There are neither { nor [ nor (, ie. we are at the toplevel.
        if paren == [0, 0]
            return 0
        endif

        " Now we have to reimplement lispindent. This is surprisingly easy, as
        " soon as one has access to syntax items.
        "
        " - Check whether we are in a special position after a word in
        "   g:clojure_special_indent_words. These are special cases.
        " - Get the next keyword after the (.
        " - If its first character is also a (, we have another sexp and align
        "   one column to the right of the unmatched (.
        " - In case it is in lispwords, we indent the next line to the column of
        "   the ( + sw.
        " - If not, we check whether it is last word in the line. In that case
        "   we again use ( + sw for indent.
        " - In any other case we use the column of the end of the word + 2.
        call cursor(paren)

        if s:IsMethodSpecialCase(paren)
            return paren[1] + &shiftwidth - 1
        endif

        " In case we are at the last character, we use the paren position.
        if col("$") - 1 == paren[1]
            return paren[1]
        endif

        " In case after the paren is a whitespace, we search for the next word.
        normal! l
        if s:CurrentChar() == ' '
            normal! w
        endif

        " If we moved to another line, there is no word after the (. We
        " use the ( position for indent.
        if line(".") > paren[0]
            return paren[1]
        endif

        " We still have to check, whether the keyword starts with a (, [ or {.
        " In that case we use the ( position for indent.
        let w = s:CurrentWord()
        if stridx('([{', w[0]) > -1
            return paren[1]
        endif

        " Test words without namespace qualifiers and leading reader macro
        " metacharacters.
        "
        " e.g. clojure.core/defn and #'defn should both indent like defn.
        let ww = substitute(w, "\\v%(.*/|[#'`~@^,]*)(.*)", '\1', '')

        if &lispwords =~ '\V\<' . ww . '\>'
            return paren[1] + &shiftwidth - 1
        endif

        if g:clojure_fuzzy_indent
            \ && !s:MatchesOne(g:clojure_fuzzy_indent_blacklist, ww)
            \ && s:MatchesOne(g:clojure_fuzzy_indent_patterns, ww)
            return paren[1] + &shiftwidth - 1
        endif

        normal! W
        if paren[0] < line(".")
            return paren[1] + &shiftwidth - 1
        endif

        normal! ge
        return virtcol(".") + 1
    endfunction

    setlocal indentexpr=GetClojureIndent()

else

    " In case we have searchpairpos not available we fall back to
    " normal lisp indenting.
    setlocal indentexpr=
    setlocal lisp
    let b:undo_indent .= '| setlocal lisp<'

endif

" Specially indented symbols from clojure.core and clojure.test.
"
" Clojure symbols are indented in the defn style when they:
"
"   * Define vars and anonymous functions
"   * Create new lexical scopes or scopes with altered environments
"   * Create conditional branches from a predicate function or value
"
" The arglists for these functions are generally in the form of [x & body];
" Functions that accept a flat list of forms do not treat the first argument
" specially and hence are not indented specially.

" Definitions
setlocal lispwords=
setlocal lispwords+=bound-fn
setlocal lispwords+=def
setlocal lispwords+=definline
setlocal lispwords+=definterface
setlocal lispwords+=defmacro
setlocal lispwords+=defmethod
setlocal lispwords+=defmulti
setlocal lispwords+=defn
setlocal lispwords+=defn-
setlocal lispwords+=defonce
setlocal lispwords+=defprotocol
setlocal lispwords+=defrecord
setlocal lispwords+=defstruct
setlocal lispwords+=deftest " clojure.test
setlocal lispwords+=deftest- " clojure.test
setlocal lispwords+=deftype
setlocal lispwords+=extend
setlocal lispwords+=extend-protocol
setlocal lispwords+=extend-type
setlocal lispwords+=fn
setlocal lispwords+=ns
setlocal lispwords+=proxy
setlocal lispwords+=reify
setlocal lispwords+=set-test " clojure.test

" Binding forms
setlocal lispwords+=as->
setlocal lispwords+=binding
setlocal lispwords+=doall
setlocal lispwords+=dorun
setlocal lispwords+=doseq
setlocal lispwords+=dotimes
setlocal lispwords+=doto
setlocal lispwords+=for
setlocal lispwords+=if-let
setlocal lispwords+=let
setlocal lispwords+=letfn
setlocal lispwords+=locking
setlocal lispwords+=loop
setlocal lispwords+=testing " clojure.test
setlocal lispwords+=when-first
setlocal lispwords+=when-let
setlocal lispwords+=with-bindings
setlocal lispwords+=with-in-str
setlocal lispwords+=with-local-vars
setlocal lispwords+=with-open
setlocal lispwords+=with-precision
setlocal lispwords+=with-redefs
setlocal lispwords+=with-redefs-fn
setlocal lispwords+=with-test " clojure.test

" Conditional branching
setlocal lispwords+=case
setlocal lispwords+=cond->
setlocal lispwords+=cond->>
setlocal lispwords+=condp
setlocal lispwords+=if
setlocal lispwords+=if-not
setlocal lispwords+=when
setlocal lispwords+=when-not
setlocal lispwords+=while

" Exception handling
setlocal lispwords+=catch
setlocal lispwords+=try " For aesthetics when enclosing single line

let &cpo = s:save_cpo
unlet! s:save_cpo

" vim:sts=4 sw=4 et:
