" ============================================================================
" File:        fswitch.vim
"
" Description: Vim global plugin that provides decent companion source file
"              switching
"
" Maintainer:  Derek Wyatt <derek at myfirstnamemylastname dot org>
"
" Last Change: March 23rd 2009
"
" License:     This program is free software. It comes without any warranty,
"              to the extent permitted by applicable law. You can redistribute
"              it and/or modify it under the terms of the Do What The Fuck You
"              Want To Public License, Version 2, as published by Sam Hocevar.
"              See http://sam.zoy.org/wtfpl/COPYING for more details.
" ============================================================================

if exists("g:disable_fswitch")
    finish
endif

if v:version < 700
  echoerr "FSwitch requires Vim 7.0 or higher."
  finish
endif

" Version
let s:fswitch_version = '0.9.5'

" Get the path separator right
let s:os_slash = &ssl == 0 && (has("win16") || has("win32") || has("win64")) ? '\' : '/'

" Default locations - appended to buffer locations unless otherwise specified
let s:fswitch_global_locs = '.' . s:os_slash

"
" s:SetVariables
"
" There are two variables that need to be set in the buffer in order for things
" to work correctly.  Because we're using an autocmd to set things up we need to
" be sure that the user hasn't already set them for us explicitly so we have
" this function just to check and make sure.  If the user's autocmd runs after
" ours then they will override the value anyway.
"
function! s:SetVariables(dst, locs)
    if !exists("b:fswitchdst")
        let b:fswitchdst = a:dst
    endif
    if !exists("b:fswitchlocs")
        let b:fswitchlocs = a:locs
    endif
endfunction

"
" s:FSGetLocations
"
" Return the list of possible locations
"
function! s:FSGetLocations()
    let locations = []
    if exists("b:fswitchlocs")
        let locations = split(b:fswitchlocs, ',')
    endif
    if !exists("b:fsdisablegloc") || b:fsdisablegloc == 0
        let locations += split(s:fswitch_global_locs, ',')
    endif

    return locations
endfunction

"
" s:FSGetExtensions
"
" Return the list of destination extensions
"
function! s:FSGetExtensions()
    return split(b:fswitchdst, ',')
endfunction

"
" s:FSGetFilenameMutations
"
" Return the list of possible filename mutations
"
function! s:FSGetFilenameMutations()
    if !exists("b:fswitchfnames")
        " For backward-compatibility out default mutation is an identity.
        return ['/^//']
    else
        return split(b:fswitchfnames, ',')
    endif
endfunction

"
" s:FSGetMustMatch
"
" Return a boolean on whether or not the regex must match
"
function! s:FSGetMustMatch()
    let mustmatch = 1
    if exists("b:fsneednomatch") && b:fsneednomatch != 0
        let mustmatch = 0
    endif

    return mustmatch
endfunction

"
" s:FSGetFullPathToDirectory
"
" Given the filename, return the fully qualified directory portion
"
function! s:FSGetFullPathToDirectory(filename)
    return expand(a:filename . ':p:h')
endfunction

"
" s:FSGetFileExtension
"
" Given the filename, returns the extension
"
function! s:FSGetFileExtension(filename)
    return expand(a:filename . ':e')
endfunction

"
" s:FSGetFileNameWithoutExtension
"
" Given the filename, returns just the file name without the path or extension
"
function! s:FSGetFileNameWithoutExtension(filename)
    return expand(a:filename . ':t:r')
endfunction

"
" s:FSMutateFilename
"
" Takes a filename and a filename mutation directive and applies the mutation
" to it.
function! s:FSMutateFilename(filename, directive)
    let separator = strpart(a:directive, 0, 1)
    let dirparts = split(strpart(a:directive, 1), separator)
    if len(dirparts) < 2 || len(dirparts) > 3
        throw 'Bad mutation directive "' . a:directive . '".'
    else
        let flags = ''
        if len(dirparts) == 3
            let flags = dirparts[2]
        endif
        return substitute(a:filename, dirparts[0], dirparts[1], flags)
    endif
endfunction

"
" s:FSGetAlternateFilename
"
" Takes the path, name and extension of the file in the current buffer and
" applies the location to it.  If the location is a regular expression pattern
" then it will split that up and apply it accordingly.  If the location pattern
" is actually an explicit relative path or an implicit one (default) then it
" will simply apply that to the file directly.
"
function! s:FSGetAlternateFilename(filepath, filename, newextension, location, mustmatch)
    let parts = split(a:location, ':')
    let cmd = 'rel'
    let directive = parts[0]
    if len(parts) == 2
        let cmd = parts[0]
        let directive = parts[1]
    endif
    if cmd == 'reg' || cmd == 'ifrel' || cmd == 'ifabs'
        if strlen(directive) < 3
            throw 'Bad directive "' . a:location . '".'
        else
            let separator = strpart(directive, 0, 1)
            let dirparts = split(strpart(directive, 1), separator)
            if len(dirparts) < 2 || len(dirparts) > 3
                throw 'Bad directive "' . a:location . '".'
            else
                let part1 = dirparts[0]
                let part2 = dirparts[1]
                let flags = ''
                if len(dirparts) == 3
                    let flags = dirparts[2]
                endif
                if cmd == 'reg'
                    if a:mustmatch == 1 && match(a:filepath, part1) == -1
                        let path = ""
                    else
                        let path = substitute(a:filepath, part1, part2, flags) . s:os_slash .
                                    \ a:filename . '.' . a:newextension
                    endif
                elseif cmd == 'ifrel'
                    if match(a:filepath, part1) == -1
                        let path = ""
                    else
                        let path = a:filepath . s:os_slash . part2 .
                                     \ s:os_slash . a:filename . '.' . a:newextension
                    endif
                elseif cmd == 'ifabs'
                    if match(a:filepath, part1) == -1
                        let path = ""
                    else
                        let path = part2 . s:os_slash . a:filename . '.' . a:newextension
                    endif
                endif
            endif
        endif
    elseif cmd == 'rel'
        let path = a:filepath . s:os_slash . directive . s:os_slash . a:filename . '.' . a:newextension
    elseif cmd == 'abs'
        let path = directive . s:os_slash . a:filename . '.' . a:newextension
    endif

    return simplify(path)
endfunction

"
" s:FSReturnCompanionFilename
"
" This function will return a path that is the best candidate for the companion
" file to switch to.  If mustBeReadable == 1 when then the companion file will
" only be returned if it is readable on the filesystem, otherwise it will be
" returned so long as it is non-empty.
"
function! s:FSReturnCompanionFilename(filename, mustBeReadable)
    let fullpath = s:FSGetFullPathToDirectory(a:filename)
    let ext = s:FSGetFileExtension(a:filename)
    let justfile = s:FSGetFileNameWithoutExtension(a:filename)
    let extensions = s:FSGetExtensions()
    let filenameMutations = s:FSGetFilenameMutations()
    let locations = s:FSGetLocations()
    let mustmatch = s:FSGetMustMatch()
    let newpath = ''
    for currentExt in extensions
        for loc in locations
            for filenameMutation in filenameMutations
                let mutatedFilename = s:FSMutateFilename(justfile, filenameMutation)
                let newpath = s:FSGetAlternateFilename(fullpath, mutatedFilename, currentExt, loc, mustmatch)
                if a:mustBeReadable == 0 && newpath != ''
                    return newpath
                elseif a:mustBeReadable == 1
                    let newpath = glob(newpath)
                    if filereadable(newpath)
                        return newpath
                    endif
                endif
            endfor
        endfor
    endfor

    return newpath
endfunction

"
" FSReturnReadableCompanionFilename
"
" This function will return a path that is the best candidate for the companion
" file to switch to, so long as that file actually exists on the filesystem and
" is readable.
"
function! FSReturnReadableCompanionFilename(filename)
    return s:FSReturnCompanionFilename(a:filename, 1)
endfunction

"
" FSReturnCompanionFilenameString
"
" This function will return a path that is the best candidate for the companion
" file to switch to.  The file does not need to actually exist on the
" filesystem in order to qualify as a proper companion.
"
function! FSReturnCompanionFilenameString(filename)
    return s:FSReturnCompanionFilename(a:filename, 0)
endfunction

"
" FSwitch
"
" This is the only externally accessible function and is what we use to switch
" to the alternate file.
"
function! FSwitch(filename, precmd)
    if !exists("b:fswitchdst") || strlen(b:fswitchdst) == 0
        throw 'b:fswitchdst not set - read :help fswitch'
    endif
    if (!exists("b:fswitchlocs")   || strlen(b:fswitchlocs) == 0) &&
     \ (!exists("b:fsdisablegloc") || b:fsdisablegloc == 0)
        throw "There are no locations defined (see :h fswitchlocs and :h fsdisablegloc)"
    endif
    let newpath = FSReturnReadableCompanionFilename(a:filename)
    let openfile = 1
    if !filereadable(newpath)
        if exists("b:fsnonewfiles") || exists("g:fsnonewfiles")
            let openfile = 0
        else
            let newpath = FSReturnCompanionFilenameString(a:filename)
        endif
    endif
    if &switchbuf =~ "^use"
        let i = 1
        let bufnum = winbufnr(i)
        while bufnum != -1
            let filename = fnamemodify(bufname(bufnum), ':p')
            if filename == newpath
                execute ":sbuffer " .  filename
                return
            endif
            let i += 1
            let bufnum = winbufnr(i)
        endwhile
    endif
    if openfile == 1
        if newpath != ''
            if strlen(a:precmd) != 0
                execute a:precmd
            endif
            let s:fname = fnameescape(newpath)

            if (strlen(bufname(s:fname))) > 0
                execute 'buffer ' . s:fname
            else
                execute 'edit ' . s:fname
            endif
        else
            echoerr "Alternate has evaluated to nothing.  See :h fswitch-empty for more info."
        endif
    else
        echoerr "No alternate file found.  'fsnonewfiles' is set which denies creation."
    endif
endfunction

"
" The autocmds we set up to set up the buffer variables for us.
"
augroup fswitch_au_group
    au!
    au BufEnter *.c    call s:SetVariables('h',       'reg:/src/include/,reg:|src|include/**|,ifrel:|/src/|../include|')
    au BufEnter *.cc   call s:SetVariables('hh',      'reg:/src/include/,reg:|src|include/**|,ifrel:|/src/|../include|')
    au BufEnter *.cpp  call s:SetVariables('hpp,h',   'reg:/src/include/,reg:|src|include/**|,ifrel:|/src/|../include|')
    au BufEnter *.cxx  call s:SetVariables('hxx',     'reg:/src/include/,reg:|src|include/**|,ifrel:|/src/|../include|')
    au BufEnter *.C    call s:SetVariables('H',       'reg:/src/include/,reg:|src|include/**|,ifrel:|/src/|../include|')
    au BufEnter *.m    call s:SetVariables('h',       'reg:/src/include/,reg:|src|include/**|,ifrel:|/src/|../include|')
    au BufEnter *.h    call s:SetVariables('c,cpp,m', 'reg:/include/src/,reg:/include.*/src/,ifrel:|/include/|../src|')
    au BufEnter *.hh   call s:SetVariables('cc',      'reg:/include/src/,reg:/include.*/src/,ifrel:|/include/|../src|')
    au BufEnter *.hpp  call s:SetVariables('cpp',     'reg:/include/src/,reg:/include.*/src/,ifrel:|/include/|../src|')
    au BufEnter *.hxx  call s:SetVariables('cxx',     'reg:/include/src/,reg:/include.*/src/,ifrel:|/include/|../src|')
    au BufEnter *.H    call s:SetVariables('C',       'reg:/include/src/,reg:/include.*/src/,ifrel:|/include/|../src|')
augroup END

"
" The mappings used to do the good work
"
com! FSHere       :call FSwitch('%', '')
com! FSRight      :call FSwitch('%', 'wincmd l')
com! FSSplitRight :call FSwitch('%', 'let curspr=&spr | set nospr | vsplit | wincmd l | if curspr | set spr | endif')
com! FSLeft       :call FSwitch('%', 'wincmd h')
com! FSSplitLeft  :call FSwitch('%', 'let curspr=&spr | set nospr | vsplit | if curspr | set spr | endif')
com! FSAbove      :call FSwitch('%', 'wincmd k')
com! FSSplitAbove :call FSwitch('%', 'let cursb=&sb | set nosb | split | if cursb | set sb | endif')
com! FSBelow      :call FSwitch('%', 'wincmd j')
com! FSSplitBelow :call FSwitch('%', 'let cursb=&sb | set nosb | split | wincmd j | if cursb | set sb | endif')
com! FSTab        :call FSwitch('%', 'tabedit')

