Friday, December 12, 2008

Searching files within Vim and opening results with Vim

When I am programming, I often swap between using Eclipse or a Cywin + UltraEdit combination.

In Eclipse, Control+Shift+T is a wild card enabled search for Java files by file name and Control+Shift+R is a wild card enabled search for everything else (plus Java files) by file name - where you can double click on a result to open the file. You also have a file search (with regex) with Control+H.

In Cygwin, I use my own script to regex search for files by name, opening whichever file I need in UltraEdit. I also have a couple of scripts that grep through file contents and pass the result to the above script, which offers me a menu of the results and allows me to choose which of the results to open. I find this incredibly handy for many situations when I want to drop to a command line and search with a regular expression, or when I want a refreshing burst of geeky joy from using a command line.

I find it is easier to use my script on the command line to search through files and open the match I need than it is to use Eclipse's file search.

Recently, I have started to use Vim more and more - a particularly useful skill for times when I need to SSH to a remote *nix box and I don't have an UltraEdit to help me. Like any program, there is a learning curve, but text editors like Vim and Emacs have a steeper curve because there are no GUI menus to explore. However, the reward is arguably higher because as you learn how to use the app, you find so many ways to make your life easier.

I decided to try and match the functionality above using Vim. Here is the way I have found to allow searching for files by name and contents, seeing a list of results and easily opening one of the results.

Put the following in your .vimrc file.


" http://vim.wikia.com/wiki/Display_shell_commands'_output_on_Vim_window
" Run a shell command and open results in a horizontal split
command! -complete=file -nargs=+ Split call s:RunShellCommandInSplit(<q-args>)
function! s:RunShellCommandInSplit(cmdline)
  botright new
  setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile nowrap
  call setline(1,a:cmdline)
  call setline(2,substitute(a:cmdline,'.','=','g'))
  execute 'silent $read !'.escape(a:cmdline,'%#')
  setlocal nomodifiable
  1
endfunction

" Copy of the above that opens results in a new tab.
command! -complete=file -nargs=+ Tab call s:RunShellCommandInTab(<q-args>)
function! s:RunShellCommandInTab(cmdline)
  tabnew
  setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile nowrap
  call setline(1,a:cmdline)
  call setline(2,substitute(a:cmdline,'.','=','g'))
  execute 'silent $read !'.escape(a:cmdline,'%#')
  setlocal nomodifiable
  1
endfunction
" Issue a find command using regex and open results in a new tab.
command! -nargs=+ Find call s:RunShellCommandInTab('find . -iregex '.<q-args>)


With these two, I can now very easily run a shell command from within Vim and have the results appear in a new buffer - either a horizontal split or a new tab. For example here are three ways to run the shell command "ls -la" from within Vim:

:Split ls -la
:Tab ls -la
:! ls -la

:Split puts the results in a new horizontal split. :Tab puts the results in a new tab. The third technique is the built in way to run a shell command from within Vim. It hides Vim i.e. shows the command line, runs the command and shows the results in the command line and displays a prompt to go back to Vim: Press ENTER or type command to continue.

Now for the kicker. If I use :Split or :Tab, I can move the cursor to rest within whichever file I am interested in and press gf to open the file.

To find a file within a project's directory tree, ls -la isn't enough: I need the find command. The two examples below are equivalent.

:Find ".*\.css"
:Tab find . -iregex ".*\.css"

The first - much shorter - example uses the last short cut now in my .vimrc file. They both do a recursive regex search for a file through the entire directory tree, beginning from the directory I opened Vim from. Again, I put my cursor within whichever result I am interested in and press gf to open the file - which replaces the search results.

Always looking for shorter shortcuts for common searches, I put this small command in my bin dir: ~/bin/fjg ("fjg" for "find Java then grep").

#!/bin/bash
find . -name "*.java" | grep -i $@

It makes it much easier to search for Java source files quickly. Now I search with a shorter string:

:Tab fjg SomeClass

If I want to search through the contents of the files and select which result file to open, this is easy too:

:Find ".*\.css" | xargs grep -l span
:Tab find . -iregex ".*\.css" | xargs grep -l span

Here are some links that helped me write this.