Unite.vimでプロジェクト内のファイル一覧を出したり、検索したりする

`:UniteWithProjectDir`を使うとプロジェクトディレクトリにターゲットを絞れるけれど、カレントディレクトリ（特に変更していない場合はVimを起動したディレクトリ）を起点としてプロジェクトディレクトリを探すので、これをカレントバッファで開いているファイルのディレクトリを起点とするようにしたい。

以下のことができるよう設定する。

* カレントバッファのファイルのディレクトリから、プロジェクトディレクトリを決定する
* VimFilerにいるときは、VimFilerの現在のディレクトリから、プロジェクトディレクトリを決定する
* 自分でプロジェクトディレクトリにマッチするパターンを設定できるようにする
* .gitignoreで設定されたファイルは無視する

まず、カレントバッファのディレクトリは

```vim
let buffer_dir = expand('%:p:h')
```

で、絶対パスで取得できる。一方、VimFilerの現在のパスはVimFilerのコードを見てみると`b:vimfiler.current_dir`というバッファのローカル変数に入っているようなので、これらを使って、現在のバッファのディレクトリを決定する。

```vim
if exists('b:vimfiler.current_dir')
    let buffer_dir = b:vimfiler.current_dir
else
    let buffer_dir = expand('%:p:h')
endif
```

次にプロジェクトディレクトリだが、.gitなどを検知してプロジェクトディレクトリをいい感じで返してくれる関数が[vital.vim](https://github.com/vim-jp/vital.vim)にあるので、vital.vimをインストールしてこれを使う。こんな感じ。

```vim
let project_dir = vital#of('vital').import('Prelude').path2project_directory(buffer_dir, 1)
```

第２引数に0以外の数字を与えると、プロジェクトディレクトリが発見できなかった時は空文字を返す。与えないとフルパスを返す（`:h Vital.Prelude.path2project_directory()`）。

プロジェクトディレクトリが発見できなかった場合は`g:project_dir_pattern`で設定した正規表現にマッチしたディレクトリをプロジェクトディレクトリにする。例えば、

```vim
let g:project_dir_pattern = '^/home/tmsanrinsha/project/[^/]\+'
```

とした場合に、`/home/tmsanrinsha/project/hoge`がプロジェクトディレクトリとして認識されるようにする。

最終的に、プロジェクトディレクトリが決定できたらプロジェクトディレクトリを、できなかったらバッファのディレクトリを返すような関数を作る。

```vim
function! GetProjectDir() abort " {{{
    if exists('b:vimfiler.current_dir')
        let l:buffer_dir = b:vimfiler.current_dir
    else
        let l:buffer_dir = expand('%:p:h')
    endif

    let l:project_dir = vital#of('vital').import('Prelude').path2project_directory(l:buffer_dir, 1)
    if empty(l:project_dir) && exists('g:project_dir_pattern')
        let l:project_dir = matchstr(l:buffer_dir, g:project_dir_pattern)
    endif

    if empty(l:project_dir)
        return l:buffer_dir
    else
        return l:project_dir
    endif
endfunction " }}}
```

変数には`l:`をつけて、関数ローカルな変数であること明示した。


次にUnite.vimの設定。以下の設定をすると`,fp`でプロジェクト内のファイル一覧でuniteバッファで表示されるようになる[^1]。

```vim
nnoremap ,fp :<C-u>call <SID>unite_file_project('-start-insert')<CR>
function! s:unite_file_project(...)
    let l:opts = (a:0 ? join(a:000, ' ') : '')
    let l:project_dir = GetProjectDir()

    execute 'Unite '.opts.' file_rec/async:'.l:project_dir
endfunction
```

uniteバッファをinsert modeで始めたくない場合は`'-start-insert'`を消す

ただし、このままだと、.gitignoreに設定されているファイルも出てきてしまうので、`git ls-files`コマンドを利用した`Unite file_rec/git`をgitリポジトリでは使うようにする。`:h unite-source-file_rec/git`に書いてあるfile_rec/gitのオプションを参考にして

```vim
nnoremap [unite]fp :<C-u>call <SID>unite_file_project('-start-insert')<CR>
function! s:unite_file_project(...)
    let l:opts = (a:0 ? join(a:000, ' ') : '')
    let l:project_dir = GetProjectDir()

    if isdirectory(l:project_dir.'/.git')
        execute 'lcd '.l:project_dir
        execute 'Unite '.opts.' file_rec/git:--cached:--others:--exclude-standard'
    else
        execute 'Unite '.opts.' file_rec/async:'.l:project_dir
    endif
endfunction
```

とする。ここでfile_rec/gitはカレントディレクトリがgitのリポジトリでないと使えないので、lcdで移動する。file_rec/gitのオプションの`--cached`はインデックスにキャッシュされたファイルを出力するオプション、`--others`は追跡されてないファイルを出力するオプション、`--exlude-standard`は.gitignoreで無視されるファイルは除いて出力するオプション。要するに、.gitignoreで無視されるファイル以外のすべてを出力する。

同様な感じでgrepの方も設定する。

```vim
nnoremap ,gp :<C-u>call <SID>unite_grep_project('-start-insert')<CR>
function! s:unite_grep_project(...)
    let opts = (a:0 ? join(a:000, ' ') : '')
    let l:project_dir = GetProjectDir()
    if !executable('ag') && isdirectory(l:project_dir.'/.git')
        execute 'Unite '.opts.' grep/git:/:--untracked'
    else
        execute 'Unite '.opts.' grep:'.l:project_dir
    endif
endfunction
```

grepの方はagがあれば、agが.gitignoreにあるファイルを無視してくれるのと、smart-caseを使いたいのでagを使う。
ない場合は`git grep`を利用するgrep/gitを使う。`/`でリポジトリのルートディレクトリをディレクトリ対象に指定し、`--untracked`で追跡していないファイルも含める。

まとめると最終的に以下のような設定になる。ちょっとuniteの設定を追加した。

```vim
" Note: Skip initialization for vim-tiny or vim-small.
if 0 | endif

if has('vim_starting')
  if &compatible
    set nocompatible               " Be iMproved
  endif

  " Required:
  set runtimepath+=~/.vim/bundle/neobundle.vim/
endif

" Required:
call neobundle#begin(expand('~/.vim/bundle/'))

" Let NeoBundle manage NeoBundle
" Required:
NeoBundleFetch 'Shougo/neobundle.vim'

" My Bundles here:
" Refer to |:NeoBundle-examples|.
" Note: You don't set neobundle setting in .gvimrc!
NeoBundle 'Shougo/vimproc.vim', {
\ 'build' : {
\     'windows' : 'tools\\update-dll-mingw',
\     'cygwin' : 'make -f make_cygwin.mak',
\     'mac' : 'make -f make_mac.mak',
\     'linux' : 'make',
\     'unix' : 'gmake',
\    },
\ }
NeoBundle 'Shougo/unite.vim'
NeoBundle 'Shougo/vimfiler.vim'
NeoBundle 'vim-jp/vital.vim'

call neobundle#end()

" Required:
filetype plugin indent on

" If there are uninstalled bundles found on startup,
" this will conveniently prompt you to install them.
NeoBundleCheck

" 設定例
" let g:project_dir_pattern = '^/home/tmsanrinsha/project/[^/]\+'

function! GetProjectDir() abort " {{{
    if exists('b:vimfiler.current_dir')
        let l:buffer_dir = b:vimfiler.current_dir
    else
        let l:buffer_dir = expand('%:p:h')
    endif

    let l:project_dir = vital#of('vital').import('Prelude').path2project_directory(l:buffer_dir, 1)
    if empty(l:project_dir) && exists('g:project_dir_pattern')
        let l:project_dir = matchstr(l:buffer_dir, g:project_dir_pattern)
    endif

    if empty(l:project_dir)
        return l:buffer_dir
    else
        return l:project_dir
    endif
endfunction " }}}


nnoremap [unite] <Nop>
nmap , [unite]

call unite#custom#profile('default', 'context', {
\   'start_insert': 1,
\   'winheight': 10,
\   'auto_resize': 1,
\ })

nnoremap [unite]fp :<C-u>call <SID>unite_file_project('-start-insert')<CR>
function! s:unite_file_project(...)
    let l:opts = (a:0 ? join(a:000, ' ') : '')
    let l:project_dir = GetProjectDir()

    if isdirectory(l:project_dir.'/.git')
        execute 'lcd '.l:project_dir
        execute 'Unite '.opts.' file_rec/git:--cached:--others:--exclude-standard'
    else
        execute 'Unite '.opts.' file_rec/async:'.l:project_dir
    endif
endfunction

if executable('ag')
    " Use ag in unite grep source.
    let g:unite_source_grep_command = 'ag'
    let g:unite_source_grep_default_opts =
    \ '-f --vimgrep --hidden --ignore ' .
    \ '''.hg'' --ignore ''.svn'' --ignore ''.git'' --ignore ''.bzr'''
    let g:unite_source_grep_recursive_opt = ''
endif

nnoremap [unite]gp :<C-u>call <SID>unite_grep_project('-start-insert')<CR>
function! s:unite_grep_project(...)
    let opts = (a:0 ? join(a:000, ' ') : '')
    let l:project_dir = GetProjectDir()
    if !executable('ag') && isdirectory(l:project_dir.'/.git')
        execute 'Unite '.opts.' grep/git:/:--untracked'
    else
        execute 'Unite '.opts.' grep:'.l:project_dir
    endif
endfunction
```

[^1]: この設定は[unite.vim の file_rec でプロジェクトのファイルを一望する - Alone Like a Rhinoceros Horn](http://d.hatena.ne.jp/h1mesuke/20110918/p1)を参考にした。