365 lines
12 KiB
VimL
365 lines
12 KiB
VimL
|
" ============================================================================
|
||
|
" 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')
|
||
|
|