scriptencoding utf-8
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" AutoClose.vim - Automatically close pair of characters: ( with ), [ with ], { with }, etc.
" Version: 2.0
" Author: Thiago Alves <talk@thiagoalves.com.br>
" Maintainer: Thiago Alves <talk@thiagoalves.com.br>
" URL: http://thiagoalves.com.br
" Licence: This script is released under the Vim License.
" Last modified: 02/02/2011
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

" check if script is already loaded
if !exists("g:debug_AutoClose") && exists("g:loaded_AutoClose")
    finish "stop loading the script"
endif
let g:loaded_AutoClose = 1

let s:global_cpo = &cpo " store compatible-mode in local variable
set cpo&vim             " go into nocompatible-mode

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

if g:AutoClosePreserveDotReg
    " Because dot register preservation code remaps escape we have to remap
    " some terminal specific escape sequences first
    if &term =~ 'xterm' || &term =~ 'rxvt' || &term =~ 'screen' || &term =~ 'linux' || &term =~ 'gnome'
        imap <silent> <Esc>OA <Up>
        imap <silent> <Esc>OB <Down>
        imap <silent> <Esc>OC <Right>
        imap <silent> <Esc>OD <Left>
        imap <silent> <Esc>OH <Home>
        imap <silent> <Esc>OF <End>
        imap <silent> <Esc>[5~ <PageUp>
        imap <silent> <Esc>[6~ <PageDown>
    endif
endif

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Functions
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! s:GetCharAhead(len)
    if col('$') == col('.')
        return "\0"
    endif
    return strpart(getline('.'), col('.')-2 + a:len, 1)
endfunction

function! s:GetCharBehind(len)
    if col('.') == 1
        return "\0"
    endif
    return strpart(getline('.'), col('.') - (1 + a:len), 1)
endfunction

function! s:GetNextChar()
    return s:GetCharAhead(1)
endfunction

function! s:GetPrevChar()
    return s:GetCharBehind(1)
endfunction

" used to implement automatic deletion of closing character when opening
" counterpart is deleted and by space expansion
function! s:IsEmptyPair()
    let l:prev = s:GetPrevChar()
    let l:next = s:GetNextChar()
    return (l:next != "\0") && (get(b:AutoClosePairs, l:prev, "\0") == l:next)
endfunction

function! s:GetCurrentSyntaxRegion()
    return synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name')
endfunction

function! s:GetCurrentSyntaxRegionIf(char)
    let l:origin_line = getline('.')
    let l:changed_line = strpart(l:origin_line, 0, col('.')-1) . a:char . strpart(l:origin_line, col('.')-1)
    call setline('.', l:changed_line)
    let l:region = synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name')
    call setline('.', l:origin_line)
    return l:region
endfunction

function! s:IsForbidden(char)
    let l:result = index(b:AutoCloseProtectedRegions, s:GetCurrentSyntaxRegion()) >= 0
    if l:result
        return l:result
    endif
    let l:region = s:GetCurrentSyntaxRegionIf(a:char)
    let l:result = index(b:AutoCloseProtectedRegions, l:region) >= 0
    return l:result || l:region == 'Comment'
endfunction

function! s:AllowQuote(char, isBS)
    let l:result = 1
    if b:AutoCloseSmartQuote
        let l:initPos = 1 + (a:isBS ? 1 : 0)
        let l:charBehind = s:GetCharBehind(l:initPos)
        let l:prev = l:charBehind
        let l:backSlashCount = 0
        while l:charBehind == '\'
            let l:backSlashCount = l:backSlashCount + 1
            let l:charBehind = s:GetCharBehind(l:initPos + l:backSlashCount)
        endwhile

        if l:backSlashCount % 2
            let l:result = 0
        else
            if a:char == "'" && l:prev =~ '[a-zA-Z0-9]'
                let l:result = 0
            endif
        endif
    endif
    return l:result
endfunction

function! s:CountQuotes(char)
    let l:currPos = col('.')-1
    let l:line = strpart(getline('.'), 0, l:currPos)
    let l:result = 0

    if l:currPos >= 0
        for [q,closer] in items(b:AutoClosePairs)
            " only consider twin pairs
            if q != closer | continue | endif

            if b:AutoCloseSmartQuote != 0
                let l:regex = q . '[ˆ\\' . q . ']*(\\.[ˆ\\' . q . ']*)*' . q
            else
                let l:regex = q . '[ˆ' . q . ']*' . q
            endif

            let l:closedQuoteIdx = match(l:line, l:regex)
            while l:closedQuoteIdx >= 0
                let l:matchedStr = matchstr(l:line, l:regex, l:closedQuoteIdx)
                let l:line = strpart(l:line, 0, l:closedQuoteIdx) . strpart(l:line, l:closedQuoteIdx + strlen(l:matchedStr))
                let l:closedQuoteIdx = match(l:line, l:regex)
            endwhile
        endfor

        for c in split(l:line, '\zs')
            if c == a:char
                let l:result = l:result + 1
            endif
        endfor
    endif
    return l:result
endfunction

" The auto-close buffer is used in a fix of the redo functionality.
" As we insert characters after cursor, we remember them and at the moment
" that vim would normally collect the last entered string into dot register
" (:help ".) - i.e. when esc or a motion key is typed in insert mode - we
" erase the inserted symbols and pretend that we have just now typed them.
" This way vim picks them up into dot register as well and user can repeat the
" typed bit with . command.
function! s:PushBuffer(char)
    if !exists("b:AutoCloseBuffer")
        let b:AutoCloseBuffer = []
    endif
    call insert(b:AutoCloseBuffer, a:char)
endfunction

function! s:PopBuffer()
    if exists("b:AutoCloseBuffer") && len(b:AutoCloseBuffer) > 0
        call remove(b:AutoCloseBuffer, 0)
    endif
endfunction

function! s:EmptyBuffer()
    if exists("b:AutoCloseBuffer")
        let b:AutoCloseBuffer = []
    endif
endfunction

function! s:FlushBuffer()
    let l:result = ''
    if exists("b:AutoCloseBuffer")
        let l:len = len(b:AutoCloseBuffer)
        if l:len > 0
            let l:result = join(b:AutoCloseBuffer, '') . repeat("\<Left>", l:len)
            let b:AutoCloseBuffer = []
            call s:EraseNCharsAtCursor(l:len)
        endif
    endif
    return l:result
endfunction

function! s:InsertStringAtCursor(str)
    let l:line = getline('.')
    let l:column = col('.')-2

    if l:column < 0
        call setline('.', a:str . l:line)
    else
        call setline('.', l:line[:l:column] . a:str . l:line[l:column+1:])
    endif
endfunction

function! s:EraseNCharsAtCursor(len)
    let l:line = getline('.')
    let l:column = col('.')-2

    if l:column < 0
        call setline('.', l:line[a:len + 1:])
    else
        call setline('.', l:line[:l:column] . l:line[l:column + a:len + 1:])
    endif
endfunction

" returns the opener, after having inserted its closer if necessary
function! s:InsertPair(opener)
    if ! b:AutoCloseOn || ! has_key(b:AutoClosePairs, a:opener) || s:IsForbidden(a:opener)
      return a:opener
    endif

    let l:save_ve = &ve
    set ve=all

    let l:next = s:GetNextChar()
    " only add closing pair before space or any of the closepair chars
    let close_before = '\s\|\V\[,.;' . escape(join(keys(b:AutoClosePairs) + values(b:AutoClosePairs), ''), ']').']'
    if (l:next == "\0" || l:next =~ close_before) && s:AllowQuote(a:opener, 0)
        call s:InsertStringAtCursor(b:AutoClosePairs[a:opener])
        call s:PushBuffer(b:AutoClosePairs[a:opener])
    endif

    exec "set ve=" . l:save_ve
    return a:opener
endfunction

" returns the closer, after having eaten identical one if necessary
function! s:ClosePair(closer)
    let l:save_ve = &ve
    set ve=all

    if b:AutoCloseOn && s:GetNextChar() == a:closer
        call s:EraseNCharsAtCursor(1)
        call s:PopBuffer()
    endif

    exec "set ve=" . l:save_ve
    return a:closer
endfunction

" in case closer is identical with its opener - heuristically decide which one
" is being typed and act accordingly
function! s:OpenOrCloseTwinPair(char)
    if s:CountQuotes(a:char) % 2 == 0
        " act as opening char
        return s:InsertPair(a:char)
    else
        " act as closing char
        return s:ClosePair(a:char)
    endif
endfunction

" maintain auto-close buffer when delete key is pressed
function! s:Delete()
    let l:save_ve = &ve
    set ve=all

    if exists("b:AutoCloseBuffer") && len(b:AutoCloseBuffer) > 0 && b:AutoCloseBuffer[0] == s:GetNextChar()
        call s:PopBuffer()
    endif

    exec "set ve=" . l:save_ve
    return "\<Del>"
endfunction

" when backspace is pressed:
" - erase an empty pair if backspacing from inside one
" - maintain auto-close buffer
function! s:Backspace()
    let l:save_ve = &ve
    let l:prev = s:GetPrevChar()
    let l:next = s:GetNextChar()
    set ve=all

    if b:AutoCloseOn && s:IsEmptyPair() && (l:prev != l:next || s:AllowQuote(l:prev, 1))
        call s:EraseNCharsAtCursor(1)
        call s:PopBuffer()
    endif

    exec "set ve=" . l:save_ve
    return "\<BS>"
endfunction

function! s:Space()
    if b:AutoCloseOn && s:IsEmptyPair()
        call s:PushBuffer("\<Space>")
        return "\<Space>\<Space>\<Left>"
    else
        return "\<Space>"
    endif
endfunction

function! s:Enter()
    if has_key(b:AutoClosePumvisible, 'ENTER') && pumvisible()
        let b:snippet_chosen = 1
        return b:AutoClosePumvisible['ENTER']
    elseif b:AutoCloseOn && s:IsEmptyPair() && stridx( b:AutoCloseExpandEnterOn, s:GetPrevChar() ) >= 0
        return "\<CR>\<Esc>O"
    endif
    return "\<CR>"
endfunction

function! s:ToggleAutoClose()
    let b:AutoCloseOn = !b:AutoCloseOn
    if b:AutoCloseOn
        echo "AutoClose ON"
    else
        echo "AutoClose OFF"
    endif
endfunction

" Parse a whitespace separated line of pairs
" single characters are assumed to be twin pairs (closer identical to
" opener)
function! AutoClose#ParsePairs(string)
    if type(a:string) == type({})
        return a:string
    elseif type(a:string) != type("")
        echoerr "AutoClose#ParsePairs(): Argument not a dictionary or a string"
        return {}
    endif

    let l:dict = {}
    for pair in split(a:string)
        " strlen is length in bytes, we want in (wide) characters
        let l:pairLen = strlen(substitute(pair,'.','x','g'))
        if l:pairLen == 1
            " assume a twin pair
            let l:dict[pair] = pair
        elseif l:pairLen == 2
            let l:dict[pair[0]] = pair[1]
        else
            echoerr "AutoClose: Bad pair string - a pair longer then two character"
            echoerr " `- String: " . a:sring
            echoerr " `- Pair: " . pair . " Pair len: " . l:pairLen
        endif
    endfor
    return l:dict
endfunction

" this function is made visible for the sake of users
function! AutoClose#DefaultPairs()
    return AutoClose#ParsePairs(g:AutoClosePairs)
endfunction

function! s:ModifyPairsList(list, pairsToAdd, openersToRemove)
    return filter(
                \ extend(a:list, AutoClose#ParsePairs(a:pairsToAdd), "force"),
                \ "stridx(a:openersToRemove,v:key)<0")
endfunction

function! AutoClose#DefaultPairsModified(pairsToAdd,openersToRemove)
    return s:ModifyPairsList(AutoClose#DefaultPairs(), a:pairsToAdd, a:openersToRemove)
endfunction

" Define variables (in the buffer namespace).
function! s:DefineVariables()
    " All the following variables can be set per buffer or global.
    " The buffer namespace is used internally
    let defaults = {
                \ 'AutoClosePairs': AutoClose#DefaultPairs(),
                \ 'AutoCloseProtectedRegions': ["Comment", "String", "Character"],
                \ 'AutoCloseSmartQuote': 1,
                \ 'AutoCloseOn': 1,
                \ 'AutoCloseSelectionWrapPrefix': '<LEADER>a',
                \ 'AutoClosePumvisible': {"ENTER": "\<C-Y>", "ESC": "\<C-E>"},
                \ 'AutoCloseExpandEnterOn': "",
                \ 'AutoCloseExpandSpace': 1,
                \ }

    " Let the user define if he/she wants the plugin to do special actions when the
    " popup menu is visible and a movement key is pressed.
    " Movement keys used in the menu get mapped to themselves
    " (Up/Down/PageUp/PageDown).
    for key in s:movementKeys
        if key == 'ENTER' || key == 'ESC'
            continue
        endif
        let defaults['AutoClosePumvisible'][key] = ''
    endfor
    for key in s:pumMovementKeys
        if key == 'ENTER' || key == 'ESC'
            continue
        endif
        let defaults['AutoClosePumvisible'][key] = '<'.key.'>'
    endfor

    if exists ('b:AutoClosePairs') && type('b:AutoClosePairs') == type("")
        let tmp = AutoClose#ParsePairs(b:AutoClosePairs)
        unlet b:AutoClosePairs
        let b:AutoClosePairs = tmp
    endif

    " Now handle/assign values
    for key in keys(defaults)
        if key == 'AutoClosePumvisible'
            let tempVisible = defaults['AutoClosePumvisible']
            if exists('g:AutoClosePumvisible') && type(eval('g:AutoClosePumvisible')) == type(defaults['AutoClosePumvisible'])
                for childKey in keys(g:AutoClosePumvisible)
                    let tempVisible[toupper(childKey)] = g:AutoClosePumvisible[childKey]
                endfor
            endif
            if exists('b:AutoClosePumvisible') && type(eval('b:AutoClosePumvisible')) == type(defaults['AutoClosePumvisible'])
                for childKey in keys(b:AutoClosePumvisible)
                    let tempVisible[toupper(childKey)] = b:AutoClosePumvisible[childKey]
                endfor
            endif
            let b:AutoClosePumvisible = tempVisible
        else
            if exists('b:'.key) && type(eval('b:'.key)) == type(defaults[key])
                continue
            elseif exists('g:'.key) && type(eval('g:'.key)) == type(defaults[key])
                exec 'let b:' . key . ' = g:' . key
            else
                exec 'let b:' . key . ' = ' . string(defaults[key])
            endif
        endif
    endfor
endfunction

function! s:CreatePairsMaps()
    " create appropriate maps to defined open/close characters
    for key in keys(b:AutoClosePairs)
        let opener = s:keyName(key)
        let closer = s:keyName(b:AutoClosePairs[key])
        let quoted_opener = s:quoteAndEscape(opener)
        let quoted_closer = s:quoteAndEscape(closer)

        exec "xnoremap <buffer> <silent> ". b:AutoCloseSelectionWrapPrefix
                    \ . opener . " <Esc>`>a" . closer .  "<Esc>`<i" . opener . "<Esc>"
        exec "xnoremap <buffer> <silent> ". b:AutoCloseSelectionWrapPrefix
                    \ . closer . " <Esc>`>a" . closer .  "<Esc>`<i" . opener . "<Esc>"
        if key == b:AutoClosePairs[key]
            exec "inoremap <buffer> <silent> " . opener
                        \ . " <C-R>=<SID>OpenOrCloseTwinPair(" . quoted_opener . ")<CR>"
        else
            exec "inoremap <buffer> <silent> " . opener
                        \ . " <C-R>=<SID>InsertPair(" . quoted_opener . ")<CR>"
            exec "inoremap <buffer> <silent> " . closer
                        \ . " <C-R>=<SID>ClosePair(" . quoted_closer . ")<CR>"
        endif
    endfor
endfunction

function! s:CreateExtraMaps()
    " Extra mapping
    inoremap <buffer> <silent> <BS>         <C-R>=<SID>Backspace()<CR>
    inoremap <buffer> <silent> <Del>        <C-R>=<SID>Delete()<CR>
    if b:AutoCloseExpandSpace
        inoremap <buffer> <silent> <Space>      <C-R>=<SID>Space()<CR>
    endif
    if len(b:AutoCloseExpandEnterOn) > 0
        inoremap <buffer> <silent> <CR>      <C-R>=<SID>Enter()<CR>
    endif

    if g:AutoClosePreserveDotReg
        " Fix the re-do feature by flushing the char buffer on key movements (including Escape):
        for key in s:movementKeys
            let l:pvisiblemap = b:AutoClosePumvisible[key]
            let key = "<".key.">"
            let l:currentmap = maparg(key,"i")
            if (l:currentmap=="")|let l:currentmap=key|endif
            if len(l:pvisiblemap)
                exec "inoremap <buffer> <silent> <expr> " . key . " pumvisible() ? '" . l:pvisiblemap . "' : '<C-R>=<SID>FlushBuffer()<CR>" . l:currentmap . "'"
            else
                exec "inoremap <buffer> <silent> " . key . "  <C-R>=<SID>FlushBuffer()<CR>" . l:currentmap
            endif
        endfor

        " Flush the char buffer on mouse click:
        inoremap <buffer> <silent> <LeftMouse>  <C-R>=<SID>FlushBuffer()<CR><LeftMouse>
        inoremap <buffer> <silent> <RightMouse> <C-R>=<SID>FlushBuffer()<CR><RightMouse>
    endif
endfunction

function! s:CreateMaps()
    silent doautocmd FileType
    call s:DefineVariables()
    call s:CreatePairsMaps()
    call s:CreateExtraMaps()

    let b:loaded_AutoClose = 1
endfunction

function! s:IsLoadedOnBuffer()
    return (exists("b:loaded_AutoClose") && b:loaded_AutoClose)
endfunction

" map some characters to their key names
function! s:keyName(char)
    let s:keyNames = {'|': '<Bar>', ' ': '<Space>'}
    return get(s:keyNames,a:char,a:char)
endfunction

" escape some characters for use in strings
function! s:quoteAndEscape(char)
    let s:escapedChars = {'"': '\"'}
    return '"' . get(s:escapedChars,a:char,a:char) . '"'
endfunction


""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Configuration
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
let s:AutoClosePairs_FactoryDefaults = AutoClose#ParsePairs("() {} [] ` \" '")
if !exists("g:AutoClosePairs_add") | let g:AutoClosePairs_add = "" | endif
if !exists("g:AutoClosePairs_del") | let g:AutoClosePairs_del = "" | endif
if !exists("g:AutoClosePairs")
    let g:AutoClosePairs = s:ModifyPairsList(
                \ s:AutoClosePairs_FactoryDefaults,
                \ g:AutoClosePairs_add,
                \ g:AutoClosePairs_del )
endif

let s:movementKeys = split('ESC UP DOWN LEFT RIGHT HOME END PAGEUP PAGEDOWN')
" list of keys that get mapped to themselves for pumvisible()
let s:pumMovementKeys = split('UP DOWN PAGEUP PAGEDOWN')


if has("gui_macvim")
    call extend(s:movementKeys, split("D-LEFT D-RIGHT D-UP D-DOWN M-LEFT M-RIGHT M-UP M-DOWN"))
endif

augroup <Plug>(autoclose)
autocmd!
autocmd BufNewFile,BufRead,BufEnter * if !<SID>IsLoadedOnBuffer() | call <SID>CreateMaps() | endif
autocmd InsertEnter * call <SID>EmptyBuffer()
autocmd BufEnter * if mode() == 'i' | call <SID>EmptyBuffer() | endif
augroup END

" Define convenient commands
command! AutoCloseOn :let b:AutoCloseOn = 1
command! AutoCloseOff :let b:AutoCloseOn = 0
command! AutoCloseToggle :call s:ToggleAutoClose()
" vim:sw=4:sts=4:
