1 " Vimball Archiver by Charles E. Campbell, Jr., Ph.D.
4 plugin/fswitch.vim [[[1
6 " ============================================================================
9 " Description: Vim global plugin that provides decent companion source file
12 " Maintainer: Derek Wyatt <derek at myfirstnamemylastname dot org>
14 " Last Change: March 23rd 2009
16 " License: This program is free software. It comes without any warranty,
17 " to the extent permitted by applicable law. You can redistribute
18 " it and/or modify it under the terms of the Do What The Fuck You
19 " Want To Public License, Version 2, as published by Sam Hocevar.
20 " See http://sam.zoy.org/wtfpl/COPYING for more details.
21 " ============================================================================
23 if exists("g:disable_fswitch")
28 let s:fswitch_version = '0.9.2'
30 " Get the path separator right
31 let s:os_slash = &ssl == 0 && (has("win16") || has("win32") || has("win64")) ? '\' : '/'
33 " Default locations - appended to buffer locations unless otherwise specified
34 let s:fswitch_global_locs = '.' . s:os_slash
39 " There are two variables that need to be set in the buffer in order for things
40 " to work correctly. Because we're using an autocmd to set things up we need to
41 " be sure that the user hasn't already set them for us explicitly so we have
42 " this function just to check and make sure. If the user's autocmd runs after
43 " ours then they will override the value anyway.
45 function! s:SetVariables(dst, locs)
46 if !exists("b:fswitchdst")
47 let b:fswitchdst = a:dst
49 if !exists("b:fswitchlocs")
50 let b:fswitchlocs = a:locs
57 " Return the list of possible locations
59 function! s:FSGetLocations()
61 if exists("b:fswitchlocs")
62 let locations = split(b:fswitchlocs, ',')
64 if !exists("b:fsdisablegloc") || b:fsdisablegloc == 0
65 let locations += split(s:fswitch_global_locs, ',')
74 " Return the list of destination extensions
76 function! s:FSGetExtensions()
77 return split(b:fswitchdst, ',')
83 " Return a boolean on whether or not the regex must match
85 function! s:FSGetMustMatch()
87 if exists("b:fsneednomatch") && b:fsneednomatch != 0
95 " s:FSGetFullPathToDirectory
97 " Given the filename, return the fully qualified directory portion
99 function! s:FSGetFullPathToDirectory(filename)
100 return expand(a:filename . ':p:h')
104 " s:FSGetFileExtension
106 " Given the filename, returns the extension
108 function! s:FSGetFileExtension(filename)
109 return expand(a:filename . ':e')
113 " s:FSGetFileNameWithoutExtension
115 " Given the filename, returns just the file name without the path or extension
117 function! s:FSGetFileNameWithoutExtension(filename)
118 return expand(a:filename . ':t:r')
122 " s:FSGetAlternateFilename
124 " Takes the path, name and extension of the file in the current buffer and
125 " applies the location to it. If the location is a regular expression pattern
126 " then it will split that up and apply it accordingly. If the location pattern
127 " is actually an explicit relative path or an implicit one (default) then it
128 " will simply apply that to the file directly.
130 function! s:FSGetAlternateFilename(filepath, filename, newextension, location, mustmatch)
131 let parts = split(a:location, ':')
133 let directive = parts[0]
136 let directive = parts[1]
138 if cmd == 'reg' || cmd == 'ifrel' || cmd == 'ifabs'
139 if strlen(directive) < 3
140 throw 'Bad directive "' . a:location . '".'
142 let separator = strpart(directive, 0, 1)
143 let dirparts = split(strpart(directive, 1), separator)
144 if len(dirparts) < 2 || len(dirparts) > 3
145 throw 'Bad directive "' . a:location . '".'
147 let part1 = dirparts[0]
148 let part2 = dirparts[1]
150 if len(dirparts) == 3
151 let flags = dirparts[2]
154 if a:mustmatch == 1 && match(a:filepath, part1) == -1
157 let path = substitute(a:filepath, part1, part2, flags) . s:os_slash .
158 \ a:filename . '.' . a:newextension
160 elseif cmd == 'ifrel'
161 if match(a:filepath, part1) == -1
164 let path = a:filepath . s:os_slash . part2 .
165 \ s:os_slash . a:filename . '.' . a:newextension
167 elseif cmd == 'ifabs'
168 if match(a:filepath, part1) == -1
171 let path = part2 . s:os_slash . a:filename . '.' . a:newextension
177 let path = a:filepath . s:os_slash . directive . s:os_slash . a:filename . '.' . a:newextension
179 let path = directive . s:os_slash . a:filename . '.' . a:newextension
182 return simplify(path)
186 " s:FSReturnCompanionFilename
188 " This function will return a path that is the best candidate for the companion
189 " file to switch to. If mustBeReadable == 1 when then the companion file will
190 " only be returned if it is readable on the filesystem, otherwise it will be
191 " returned so long as it is non-empty.
193 function! s:FSReturnCompanionFilename(filename, mustBeReadable)
194 let fullpath = s:FSGetFullPathToDirectory(a:filename)
195 let ext = s:FSGetFileExtension(a:filename)
196 let justfile = s:FSGetFileNameWithoutExtension(a:filename)
197 let extensions = s:FSGetExtensions()
198 let locations = s:FSGetLocations()
199 let mustmatch = s:FSGetMustMatch()
201 for currentExt in extensions
203 let newpath = s:FSGetAlternateFilename(fullpath, justfile, currentExt, loc, mustmatch)
204 if a:mustBeReadable == 0 && newpath != ''
206 elseif a:mustBeReadable == 1
207 let newpath = glob(newpath)
208 if filereadable(newpath)
219 " FSReturnReadableCompanionFilename
221 " This function will return a path that is the best candidate for the companion
222 " file to switch to, so long as that file actually exists on the filesystem and
225 function! FSReturnReadableCompanionFilename(filename)
226 return s:FSReturnCompanionFilename(a:filename, 1)
230 " FSReturnCompanionFilenameString
232 " This function will return a path that is the best candidate for the companion
233 " file to switch to. The file does not need to actually exist on the
234 " filesystem in order to qualify as a proper companion.
236 function! FSReturnCompanionFilenameString(filename)
237 return s:FSReturnCompanionFilename(a:filename, 0)
243 " This is the only externally accessible function and is what we use to switch
244 " to the alternate file.
246 function! FSwitch(filename, precmd)
247 if !exists("b:fswitchdst") || strlen(b:fswitchdst) == 0
248 throw 'b:fswitchdst not set - read :help fswitch'
250 if (!exists("b:fswitchlocs") || strlen(b:fswitchlocs) == 0) &&
251 \ (!exists("b:fsdisablegloc") || b:fsdisablegloc == 0)
252 throw "There are no locations defined (see :h fswitchlocs and :h fsdisablegloc)"
254 let newpath = FSReturnReadableCompanionFilename(a:filename)
256 if !filereadable(newpath)
257 if exists("b:fsnonewfiles") || exists("g:fsnonewfiles")
260 let newpath = FSReturnCompanionFilenameString(a:filename)
265 if strlen(a:precmd) != 0
268 execute 'edit ' . fnameescape(newpath)
270 echoerr "Alternate has evaluated to nothing. See :h fswitch-empty for more info."
273 echoerr "No alternate file found. 'fsnonewfiles' is set which denies creation."
278 " The autocmds we set up to set up the buffer variables for us.
280 augroup fswitch_au_group
282 au BufEnter *.h call s:SetVariables('cpp,c', 'reg:/include/src/,reg:/include.*/src/,ifrel:|/include/|../src|')
283 au BufEnter *.c,*.cpp call s:SetVariables('h', 'reg:/src/include/,reg:|src|include/**|,ifrel:|/src/|../include|')
287 " The mappings used to do the good work
289 com! FSHere :call FSwitch('%', '')
290 com! FSRight :call FSwitch('%', 'wincmd l')
291 com! FSSplitRight :call FSwitch('%', 'vsplit | wincmd l')
292 com! FSLeft :call FSwitch('%', 'wincmd h')
293 com! FSSplitLeft :call FSwitch('%', 'vsplit | wincmd h')
294 com! FSAbove :call FSwitch('%', 'wincmd k')
295 com! FSSplitAbove :call FSwitch('%', 'split | wincmd k')
296 com! FSBelow :call FSwitch('%', 'wincmd j')
297 com! FSSplitBelow :call FSwitch('%', 'split | wincmd j')
301 2fswitch.txt* For Vim version 7.2 and above Last change: 2009 Mar 23
307 Author: Derek Wyatt (derek at myfirstnamemylastname dot org)
310 Copyright: The VIM LICENSE applies to fswitch.vim, and fswitch.txt
311 (see |copyright|) except use "fswitch" instead of "Vim".
312 No warranty, express or implied.
313 Use At-Your-Own-Risk!
315 ==============================================================================
316 *fswitch* *fswitch-contents*
319 1. Contents .............................: |fswitch-contents|
320 2. About ................................: |fswitch-about|
321 3. Features .............................: |fswitch-features|
322 4. Setup ................................: |fswitch-setup|
323 5. Configuration ........................: |fswitch-configure|
324 6. "Creating" the Alternate File ........: |fswitch-altcreate|
325 7. Useful Mappings ......................: |fswitch-mappings|
326 8. FSwitch() ............................: |fswitch-function|
327 9. FSReturnCompanionFilenameString().....: |fswitch-getcompanion|
328 10. FSReturnReadableCompanionFilename()...: |fswitch-getreadablecomp|
329 11. The Default Settings .................: |fswitch-defaults|
330 12. Examples .............................: |fswitch-examples|
331 13. Troubleshooting ......................: |fswitch-trouble|
332 A. Change History .......................: |fswitch-changes|
334 ==============================================================================
338 FSwitch is designed to allow you to switch between companion files of source
339 code (e.g. "cpp" files and their corresponding "h" files). The source for
340 this came from a home-grown script that was influenced later by the
341 "Alternate" (a.vim) script.
343 The original intention was to modify the existing a.vim script to do what the
344 home-grown version could do (choose to open the other file in an existing
345 window) but it was a rather complex script and modification looked difficult
346 so the choice was made to simply move the home-grown script forward a couple
347 of notches and produce a new plugin. This doc file is twice the length of the
348 actual code at this point :)
350 ==============================================================================
354 FSwitch has the following features:
356 - Switches between a file and its companion file
357 - Ability to create a new file using a preferential location
358 - Simple configuration using buffer-local variables
359 - It's got a really long doc file (... seriously, why is this thing so
361 - Umm... other stuff?
363 ==============================================================================
367 Most of the behaviour of FSwitch is customized via buffer-local variables.
368 You set up the variables with auto commands:
370 au! BufEnter *.cpp let b:fswitchdst = 'hpp,h' | let b:fswitchlocs = '../inc'
372 That |:autocmd| will set the 'fswitchdst' and 'fswitchlocs' variables when the
373 |BufEnter| event takes place on a file whose name matches {*.cpp} (e.g. when
374 you enter the buffer containing the {MyFile.cpp} file).
376 The variables above state that the alternate file to {MyFile.cpp} are
377 {MyFile.hpp} and {MyFile.h} preferred in that order, and located in the {inc}
378 directory at the same level as the current directory.
380 That should get you there but there's more capability here if you want. To
381 get that move on to |fswitch-configure|.
383 ==============================================================================
389 'fswitchdst' string (default depends on file in current buffer)
392 The 'fswitchdst' variable denotes the file extension that is the
393 target extension of the current file's companion file. For example:
395 :let b:fswitchdst = 'cpp,cxx,C'
397 The above specifies that the current buffer's file has a companion
398 filename which can be found by replacing the current extension with
399 {cpp}, {cxx} or {C}. The extensions will be tried in this order and
400 the first match wins.
402 'fswitchdst' is taken relative to directories that are found in the
403 'fswitchlocs' variable.
406 'fswitchlocs' string (default depends on filen in current buffer)
409 The 'fswitchlocs' variable contains a set of directives that indicate
410 directory names that should be formulated when trying to find the
411 alternate file. For example:
413 " Create the destination path by substituting any
414 " 'include' string from the pathname with 'src'
415 :let b:fswitchlocs = 'reg:/include/src/'
417 " First try adding the relative path '../src' to the path
418 " in which the file in the buffer exists and if that fails
419 " then try using 'source' instead
420 :let b:fswitchlocs = 'rel:../src,source'
422 " Same as above but leaving off the optional 'rel:'
423 :let b:fswitchlocs = '../src,../source'
425 The following types of directives are understood:
429 A regular expression. The regular expression takes the form:
431 {delim}{pat}{delim}{globsub}{delim}
435 {delim} is something that doesn't appear in {pat} or
436 {globsub} used to delimit the {pat} and {globsub}
438 {pat} is a standard pattern to search on
440 {globsub} is a substitution string that will be run through
441 the |glob()| function.
445 A relative path. The {rel:} is actually optional. If you
446 leave this off, then FSwitch will assume that the string is
447 denoting a relative path.
451 Takes the same form as {:reg} but the {globsub} part of the
452 directive is a relative path. The relative path is only used
453 if the {pat} matches the existing path of the buffer.
457 An absolute path. I have no idea why you'd ever want to do
458 this, but it's there if you want it.
462 Takes the same form as {:reg} but the {globsub} part of the
463 directive is an absolute path. The absolute path is only used
464 if the {pat} matches the existing path of the buffer.
466 Why use the "if" variants?
468 Here's the situation: You've got the following file:
472 And you've set the following locations:
474 For .h -> reg:/include/src/,../src,./
475 For .cpp -> reg:/src/include/,../include,./
477 Here's what happens when run the following commands:
480 # Creates a new file ~/src/MyFile.cpp due to the first
481 # relative path in the list for .h
483 # Creates a new file ~/include/MyFile.h due to the first
484 # regular expression in the list for .cpp
486 The problem is that you've unconditionally said you want to use
487 {../src} for the alternate file but in reality you probably wanted to
488 use {./}. If you use {:ifrel} instead then you can say that you only
489 want to use {../src} if the path to the current buffer contains
490 {/include/} or something like that. If you did this FSwitch would not
491 have taken {../src} for the new file but would have chosen {./}
493 So the "right" setup is:
495 For .h -> reg:/include/src/,ifrel:|/include/|../src|,./
496 For .cpp -> reg:/src/include/,ifrel:|/src/|../include|,./
498 *'fswitchdisablegloc'*
503 Disables the appending of the default global locations to the local
504 buffer definition. Normally when processing alternate file locations
505 FSwitch will append some default values to the list of locations. If
506 you define this variable then this will not happen.
508 The default locations are currently set to "./" or ".\" depending on
509 what slash your configuration evaluates to.
511 *'fswitchnonewfiles'*
514 local to buffer and global
516 This variable is both global and local. If you want to disable the
517 creation of the alternate file when it doesn't already exist you can
518 choose to do this on a per-extension basis or globally. Set the
519 global one to shut it off all the time and use the buffer version to
525 local to buffer and global
527 Normally when doing a regular expression alteration of the path (see
528 {reg:} in 'fswitchdst' the pattern you're going to substitute the
529 value with must actually match in the string. When it doesn't matter
530 whether or not that the match actually takes place, you can set this
533 If you do set this then the failure to match actually results in
534 nothing happening at all. So if the right filename exists in the same
535 directory as the one you're switching from then that's the one that
541 If the b:fswitchlocs is set to
543 reg:/src/include/,include
547 # This is the file we're editing
548 ~/code/program/myfile.c
550 # These choices exist for the header file
551 ~/code/program/myfile.h
552 ~/code/program/include/myfile.h
554 Then the first substitution will result in the first header file being
555 chosen, not the second.
557 ==============================================================================
559 6. "Creating" the Alternate File~
561 If the file being switched to does not exist, and 'fsnonewfiles' has not been
562 set, then it will be created as a new, unwritten buffer. If there are
563 multiple possibilities here, FSwitch prefers the first possible match. For
564 example if the current buffer has a filename called {/code/src/a/b/MyFile.cpp}
565 and has the following set:
567 let b:fswitchdst = 'h,hpp'
568 let b:fswitchlocs = 'reg:/src/include/,../include,../inc'
570 then the created filename will be {/code/include/a/b/MyFile.cpp}.
572 As stated, this file hasn't actually been written to yet so you could easily
573 delete the buffer and there's no harm done but you also may not be able to
574 write the buffer very easily if the directory hierarchy doesn't yet exist. In
575 this case, it's quite helpful to define a mapping for easily creating the
578 nmap <Leader>md :!mkdir -p %:p:h<cr>
580 Then it's pretty easy to create the directory before writing the file.
582 ==============================================================================
586 I didn't bother putting mappings into the script directly as this might have
587 caused conflicts and I don't know how to avoid those. I use the following
590 - Switch to the file and load it into the current window >
591 nmap <silent> <Leader>of :FSHere<cr>
593 - Switch to the file and load it into the window on the right >
594 nmap <silent> <Leader>ol :FSRight<cr>
596 - Switch to the file and load it into a new window split on the right >
597 nmap <silent> <Leader>oL :FSSplitRight<cr>
599 - Switch to the file and load it into the window on the left >
600 nmap <silent> <Leader>oh :FSLeft<cr>
602 - Switch to the file and load it into a new window split on the left >
603 nmap <silent> <Leader>oH :FSSplitLeft<cr>
605 - Switch to the file and load it into the window above >
606 nmap <silent> <Leader>ok :FSAbove<cr>
608 - Switch to the file and load it into a new window split above >
609 nmap <silent> <Leader>oK :FSSplitAbove<cr>
611 - Switch to the file and load it into the window below >
612 nmap <silent> <Leader>oj :FSBelow<cr>
614 - Switch to the file and load it into a new window split below >
615 nmap <silent> <Leader>oJ :FSSplitBelow<cr>
617 ==============================================================================
621 The main work is done by the FSwitch() function. The reason it's documented
622 here is because you can use it to do something more interesting if you wish.
623 As it stands now, you get the "Split Above and Switch" functionality by
624 calling FSwitch() like this:
626 FSwitch('%', 'split \| wincmd k')
628 There's probably not much to stop anyone from doing something more interesting
629 in the second argument. If this string is non-empty then it will be run
630 through an |:execute| call.
632 ==============================================================================
633 *fswitch-getcompanion* *FSReturnCompanionFilenameString()*
635 9. FSReturnCompanionFilenameString()~
637 This function is used by |FSwitch()| to return the pathname to the preferred
638 companion file. In this case, the file need not actually exist on the
639 filesystem but would be the one created if you chose to do so. As an
642 let path = FSReturnCompanionFilenameString('%')
644 The resultant path string contains the preferred companion file or nothing if
645 no preferred file could be discovered.
647 ==============================================================================
648 *fswitch-getreadablecomp* *FSReturnReadableCompanionFilename()*
650 10. FSReturnReadableCompanionFilename()~
652 This function returns the companion file, but the companion file must be
653 readable on the filesystem for it to be successfully returned.
655 let path = FSReturnReadableCompanionFilename('%')
657 The resultant path string contains the preferred companion file or nothing if
658 no preferred file could be found on the filesystem.
660 In order to see what created the need for this function, see
663 ==============================================================================
665 11. The Default Settings~
667 By default FSwitch handles {c} and {cpp} files, favouring {cpp}.
671 let b:fswitchdst = 'cpp,c'
672 let b:fswitchlocs = 'reg:/include/src/,reg:/include.*/src/,../src'
676 let b:fswitchdst = 'h'
677 let b:fswitchlocs = 'reg:/src/include/,reg:|src|include/**|,../include'
682 let b:fswitchdst = 'h'
683 let b:fswitchlocs = 'reg:/src/include/,reg:|src|include/**|,../include'
686 ==============================================================================
690 Let's say you have a C++ codebase and it has the following properties (this
691 level of insanity is a bit high but versions that are only slightly saner
694 - Source files with {.cpp}, {.cc} and {.C} extensions
695 - Header files with {.h} extensions
696 - Source files and header files in the same directory
697 - Source files in the {src} directory and include files in the
699 - Source files in the {src} directory and include files in the
700 {include/name/space} directory (i.e. subdirectories denoted by the
702 - Source files in {src/name/space} and header files in
703 {include/name/space} (i.e. subdirectories denoted by the namespace).
705 As a final part to this, the "new" way of doing things in this source tree is
706 to put header files in a directory noted by namespace and to do the same with
707 source files and to name source files with a {cpp} extension.
709 In order to switch between files organized like this, you could specify the
714 au BufEnter *.h let b:fswitchdst = 'cpp,cc,C'
715 au BufEnter *.h let b:fswitchlocs = 'reg:/include/src/,reg:/include.*/src/'
718 Here the setting of b:fswitchdst to {cpp,cc,C} handles the different C++
719 extensions, and prefers to use {cpp} and will create new files with that
722 The fswitchlocs setting allows for the following:
726 Take the pathname to the file in the current buffer and
727 substitute "src" for "include". This handles the following
730 - Files are in {include} and {src} respectively
731 - Files are in {include/name/space} and {src/name/space}
736 Take the pathname to the file in the current buffer and
737 substitute "src" for "include.*". This handles the following
740 - Files are in {include/name/space} and {src} respectively
743 This one's a hiddden option. The default location is the
744 current directory already so we don't explicitly have to state
745 this, but it is the last possibility:
747 - Files are in the same directory
750 Here we'll just show a quick example of making use of the globbing aspect of
751 the system. Let's say you're working on a {cpp} file and you want to find the
752 matching header file, and you have your destination and locations set to the
755 let b:fswitchdst = 'h'
756 let b:fswitchlocs = 'reg:|src|include/**|'
758 then if you have the a file {src/MyFile.cpp} then this will find the file
759 {include/name/space/MyFile.h}.
762 At work I'm a Windows C++ programmer and at home I'm a OS X Objective-C
763 programmer. There's a problem with this... C++ and Objective-C both use the
764 same extension for header files ({.h}).
766 At home I want to be able to use the XCode command line builder in the
767 'makeprg' setting when I'm working on the code. I would like this to be set
768 when I am on a {.m} file or its companion {.h} file. This is done with the
771 function! SetMakeForXCode(filename)
773 let ext = expand(a:filename . ":e")
774 if ext == 'm' || ext == 'mm'
777 " Find the companion file
778 let companionfile = FSReturnReadableCompanionFilename('%')
779 " For some reason expand() doesn't work on the next line
780 let companionext = substitute(companionfile, '.*\.', '', '')
781 if companionext == 'm' || companionext == 'mm'
786 setl makeprg=xcodebuild\ -configuration\ Debug
790 Yup, this could have been easier by using the 'filetype' or using some sort of
791 |grep| call but I wanted to use this particular hammer. :) I'll probably end
792 up switching it to use the 'filetype' instead in the end...
794 ==============================================================================
798 You may get the following error:
800 Alternate has evaluated to nothing. See :h fswitch-empty for more info.
802 It can happen... This is probably due to the fact that you've got a nicely
803 strict set of rules for your locations. With |fswitch-reg| and
804 |fswitch-ifrel| and |fswitch-ifabs| you can get rather specific about whether
805 or not anything actually happens. If you aren't letting anything really
806 happen, it's not going to happen and you're going to end up with an empty
809 ==============================================================================
814 - Fix for the splitting commands (Thanks Michael Henry)
817 - Added :ifrel (|fswitch_ifrel|)
818 - Added :ifabs (|fswitch_ifabs|)
819 - Added |FSReturnReadableCompanionFilename()|
820 - Added |FSReturnCompanionFilenameString()|
821 - Changed default settings for .h to use :ifrel instead of :rel
822 - Changed default settings for .c and .cpp to use :ifrel instead of
828 vim:tw=78:sts=8:ts=8:sw=8:noet:ft=help: