8

I normally work with 2 windows, vertically split.

The problem I would like to solve, is: easily jumping back and forth from header <-> implementation file

I'm investigating, without luck, 2 ways to do this:

  1. Open alternate file in current window: There are many ways to do this, however, I can't find a way that remembers where I was in the previous file (i.e. jump to header, jump back, but not to the same place I was).
  2. Open alternate file in the other window: This is well-defined, since I only ever work with 2 windows, however I lack the vim-knowledge to do this.
5

There's three main steps in accomplishing what you're asking:

  • getting the alternate file's name
  • opening that file in the current window or in another window as desired
  • restoring cursor position within that file

To find the alternate file name you want to break the current file name into the "root" and the "extension." A simple way to do this is:

let parts = split(expand("%:p"), "[.]");
let root = parts[0]
let extension = parts[1]

If you know you're only ever switching between .h and .cpp files, you can change the extension from one to the other easily:

if extension == "h"
  let extension = "cpp"
else
  let extension = "h"
endif

Alternatively, create a dictionary mapping known extensions to potentially valid alternate extensions. Or use globpath() to get all possible alternates for the current file:

let alternates = globpath(expand("%:h"), root . ".*")

and pick the first one, or whatever. I prefer the globpath approach, with some extra smarts I'll describe later. Once you have picked the target extension, form the full target path:

let target = root . "." . alternates[whicheverAlternateExtensionYouWant]

Now you can open the alternate file in the current window:

execute "edit " . target

Or use winnr() to get the "other window" number (winnr("#") is the window that <C-W>p would jump to, or you can hard-code it if you know it will always be the same for your setup) and do something like:

let window = winnr("#")
execute window . "wincmd w"
execute "edit " . target

This gives you a really basic solution to opening alternate files. There's a few deficiencies with the above approach, since I wrote it to be straightforward and it's a bit off-the-cuff. I've written a plugin that does alternate file switching "the way I wanted," (cycling through all available globpath() results). It addresses some of the problems with the simplicity of the above, you can check out its implementation if you're interested in exploring more.

Finally, the "restore cursor position" point. I saved it for last since it's orthogonal to the alternate switching thing (my plugin doesn't handle it for example), but you could put it into your function if you're going to roll your own. :help line() has an autocommand that is useful for restoring the cursor position to where it was when the file was last opened:

:au BufReadPost * if line("'\"") > 1 && line("'\"") <= line("$") | exe "normal! g`\"" | endif

I just put that, or something very similar, in my .vimrc since I prefer the behavior all the time. You could easily just put the code elsewhere though.

2

You can use the vim-fswitch plugin together with the following configuration in your .vimrc file:

au! BufEnter *.cpp,*.cc,*.c let b:fswitchdst = 'h,hpp'    | let b:fswitchlocs = 'reg:/src/include/,../include,./'
au! BufEnter *.h,*.hpp      let b:fswitchdst = 'cpp,cc,c' | let b:fswitchlocs = 'reg:/include/src/,../src,./'

nmap <silent> <Leader>s :FSHere<cr>

If you type <Leader> (which is \ by default) followed by s in a .hpp or .h file, the plugin will check if a matching .cpp, .cc or .c file exists:

  • by replacing include by src in the current file's path
  • by looking in the src folder above the current file
  • by looking in the current file's folder

There are more options you could use to fit your project better in the documentation. It'll take you a few minutes but once you'll get it right you should love it. I personally find it very flexible and extensible, plus it works just as well for many kinds of files (.m, .h, .inl etc...).

1

In your .vimrc

" =====[ Remap to change windows quickly ]=============================
:nnoremap <silent> <C-H> :wincmd h<CR>
:nnoremap <silent> <C-J> :wincmd j<CR>
:nnoremap <silent> <C-K> :wincmd k<CR>
:nnoremap <silent> <C-L> :wincmd l<CR>

This allows you to move between windows quickly, by simply using Ctrl and VIM direction keys from the home row. The awesomeness of this, is you have one common way to jump to any window, including the quickfix window.

To quickly switch between header and source I use vim-scripts/a.vim found here: https://github.com/vim-scripts/a.vim, use :A command to toggle.

Side Note => If you use tmux, you can use https://github.com/christoomey/vim-tmux-navigator to jump between vim or nvim windows and a terminal seamlessly.

1

I'll just share my super quick&dirty version...

Set up my mappings; alt-o opens the related file in the same window, alt-shift-o opens in a split...

nnoremap <A-o> :call EditRelatedFile()<CR>
nnoremap <A-O> :call SplitRelatedFile()<CR>

Then I have a function that gets the list of related files. I've been meaning to tweak it to chop the filename at the first dot rather than the last but that's still an exercise for the reader.

function! GetRelatedFileList()
    " This function may be overloaded in a site-specific vimrc.
    let l:thisPath = expand("%:p:r") . '.*'
    let l:files = glob(l:thisPath)

    return split(l:files, '[\r\n]\+')
endfunction

My .vimrc setup is sometimes specialized per client; some have source and includes in separate folder hierarchies, some have them together. By default I assume they're all nearby but if I need to hunt I'll provide a replacement function like this one.

" Override the basic GetRelatedFileList coming from vimrc.
function! GetRelatedFileList()
    let l:thisDir = expand("%:p:h:t")
    if (l:thisDir ==? "src") || (l:thisDir ==? "include")
        let l:thisPath = expand("%:p:h:h")
        let l:searchPaths = l:thisPath.'/include,' . l:thisPath.'/src'
        let l:thisBase = expand("%:t:r") . '.*'
        let l:files = globpath(l:searchPaths, l:thisBase)
    else
        let l:thisPath = expand("%:p:r") . '.*'
        let l:files = glob(l:thisPath)
    endif

    return split(l:files, '[\r\n]\+')
endfunction

Then I look through that list of files to find the current buffer's file and go to the next one in the list. It's not often as simple as a .cpp/.h pair, often I'll have other things that need to be considered.

function! GetNextRelatedFile()
    let l:fileList = GetRelatedFileList()

    let l:thisFile = expand("%:p")
    let l:index = index(l:fileList, l:thisFile) + 1

    if l:index >= len(l:fileList)
        let l:index = 0
    endif

    return l:fileList[l:index]
endfunction

And then finally the two functions that either open in the current window or do a split...

function! EditRelatedFile()
    let l:file = GetNextRelatedFile()
    execute "edit" l:file
endfunction

My splitting version always puts .cpp and .c files in a below split, otherwise split default (which in my case is above).

function! SplitRelatedFile()
    let l:file = GetNextRelatedFile()
    let l:ext = fnamemodify(l:file, ":e")
    if (l:ext ==? "cpp") || (l:ext ==? "c")
        execute "below split" l:file
    else
        execute "split" l:file
    endif
endfunction

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.