汎用fzfでバッファ内ファジー検索をダイナミックに

## [yuki-yano/fzf-preview.vim](https://github.com/yuki-yano/fzf-preview.vim)みたいなことをしたかった
プラグインを入れずvimスクリプトのみでやったりました。
いきなりですがレポとソースはこちら（長いので折りたたみ）

https://github.com/serna37/vim

<details><summary>vimrcで呼べる汎用fzf関数</summary>

<div>

```汎用検索.vim

" ===================================================================
" junegunn/fzf.vim
" ===================================================================
" {{{
" usage
" let arguments_def = #{
"     \ title: popup title as String,
"     \ list: search targets as List,
"     \ type: format in list. f, lm, flm (file, line, msg),
"     \ eprfx: enter zone prefix text as String (0 to default value),
"     \ }
" and call like this.
" cal s:fzsearch.popup(arguments_def)

let s:fzsearch = #{bwid: 0, ewid: 0, rwid: 0, pwid: 0, tid: 0, res: [],
    \ ffdict: #{vimrc: 'vim', zshrc: 'sh', js: 'javascript', py: 'python', md: 'markdown',
        \ ts: 'typescript', tsx: 'typescriptreact',
        \ },
    \ }

let s:fzsearch.allow_exts = glob($VIMRUNTIME.'/ftplugin/*.vim')->split('\n')
            \ ->map({_,v->matchstr(v,'[^/]\+\.vim')->split('\.')[0]})

fu! s:fzsearch.popup(v) abort
    if empty(a:v.list) || (len(a:v.list) == 1 && empty(a:v.list[0]))
        cal s:echoE('no data')
        retu
    endif

    let self.list = a:v.list
    let self.type = a:v.type
    " TODO depends on type
    let self.prr = a:v.eprfx
    let self.wd = ''
    let self.wda = []
    let self.ridx = 0
    let self.max = 30
    let self.mode = 'Fuzzy'
    let self.pr = '['.self.mode.']'.(self.prr ?? '>>')
    let self.res = a:v.list[0:self.max]

    let self.bwid = popup_create([], #{title: ' '.a:v.title.' ',
        \ zindex: 50, mapping: 0, scrollbar: 0,
        \ border: [], borderchars: ['─','│','─','│','╭','╮','╯','╰'], borderhighlight: ['FzBWin'],
        \ minwidth: &columns*9/12, maxwidth: &columns*9/12,
        \ minheight: &lines/2+6, maxheight: &lines/2+6,
        \ line: &lines/4-2, col: &columns/8+1,
        \ })

    let self.ewid = popup_create(self.pr, #{title: ' Search Mode: <Tab> | ClearText: <C-w> ',
        \ zindex: 100, mapping: 0,
        \ border: [], borderchars: ['─','│','─','│','╭','╮','╯','╰'], borderhighlight: ['FzEWin'],
        \ minwidth: &columns/3, maxwidth: &columns/3,
        \ minheight: 1, maxheight: 1,
        \ line: &lines*3/4+2, col: &columns/7+1,
        \ filter: function(self.fil, [#{wd: []}]),
        \ })
    cal matchaddpos('FzEWin', [[1, 1, len(self.pr)]],  16, -1, #{window: self.ewid})
    cal matchadd('DarkRed', '\[.*\]', 17, -1, #{window: self.ewid})

    let self.rwid = popup_menu(self.res, #{title: ' Choose: <C-n/p> <CR> | QuickFix: <C-q> ',
        \ zindex: 99, mapping: 0, scrollbar: 1,
        \ border: [], borderchars: ['─','│','─','│','╭','╮','╯','╰'], borderhighlight: ['FzWin'],
        \ minwidth: &columns/3, maxwidth: &columns/3,
        \ minheight: &lines/2, maxheight: &lines/2,
        \ pos: 'topleft', line: &lines/4, col: &columns/7,
        \ callback: 'Fzsearch_confirm',
        \ filter: function(self.jk, [0]),
        \ })

    " result list syntax
    if self.type == 'lm'
        cal setbufvar(winbufnr(self.rwid), '&filetype', &filetype)
    elseif self.type == 'flm'
        cal setbufvar(winbufnr(self.rwid), '&filetype', 'txt')
    endif

    " get preview file
    let path = bufname('%')
    if self.type =~ 'flm'
        let path = split(self.res[0], '|')[0]
    elseif self.type =~ 'f'
        let path = substitute(self.res[0], '\~', $HOME, 'g')
    endif
    let read = ['Cannot open file.', 'please check this file path.', path]
    try
        let read = readfile(path)
    catch
    endtry

    " preview
    let self.pwid = popup_menu(read, #{title: ' File Preview | Scroll: <C-d/u> ',
        \ zindex: 98, mapping: 0, scrollbar: 1,
        \ border: [], borderchars: ['─','│','─','│','╭','╮','╯','╰'], borderhighlight: ['FzWin'],
        \ minwidth: &columns/3, maxwidth: &columns/3,
        \ minheight: &lines/2+3, maxheight: &lines/2+3,
        \ pos: 'topleft', line: &lines/4, col: &columns/2+1,
        \ firstline: 1,
        \ filter: function(self.pv, [0]),
        \ })

    " get preview ext for syntax highlight
    let ext = matchstr(path, '[^\.]\+$')
    let ext = ext !~ '^[a-zA-Z0-9]\+$' ? '' : ext
    let ext = get(self.ffdict, ext, ext)
    cal setbufvar(winbufnr(self.pwid), '&filetype', self.type =~ 'f' ? ext : &filetype)

    " first line
    let lnm = 1
    if self.type == 'flm'
        let sep = split(self.res[0], '|')
        let lnm = len(sep) < 2 ? 1 : split(sep[1], ' ')[0]
    elseif self.type == 'lm'
        let lnm = split(self.res[0], ':')[0]
    endif
    " cursor line top from 10row
    cal popup_setoptions(self.pwid, #{firstline: (lnm-10 > 0 ? lnm-10 : 1)})
    cal win_execute(self.pwid, 'exe '.lnm)
endf

" on confirm
fu! Fzsearch_confirm(wid, idx) abort
    if a:idx == -1
        cal s:echoE('cancel')
        cal s:fzsearch.finalize()
        retu
    endif
    let result = s:fzsearch.res[a:idx-1]

    if s:fzsearch.type == 'f'
        exe 'e '.result
    elseif s:fzsearch.type == 'lm'
        let l = split(result, ':')[0]
        cal s:echoI('jump to '.l)
        exe l
        if s:fzsearch.mode == 'Simply'
            let @/ = s:fzsearch.wd
            norm! n
            cal s:quickhl.set()
        endif
    elseif s:fzsearch.type == 'flm'
        let sep = split(result, '|')
        let fnm = substitute(sep[0], $HOME, '~', 'g')
        let lnm = len(sep) < 2 ? 1 : split(sep[1], ' ')[0]
        " if quickfix window
        if expand('%')->empty()
            wincmd w
        endif
        exe 'e '.fnm
        exe lnm
        cal s:echoI('jump to '.fnm.' line:'.lnm)
    endif
    cal s:fzsearch.finalize()
endf

" create quickfix
fu! s:fzsearch.quickfix() abort
    let tmp = &errorformat
    let resource = self.res
    let ef = '%f: %l: %m'

    if self.type == 'lm'
        let fnm = expand('%')
        let resource = deepcopy(self.res)->map({_,v->fnm.': '.v})
    elseif self.type == 'f'
        let resource = deepcopy(self.res)->map({_,v->v.': 1: open'})
    elseif self.type == 'flm'
        " sample
        " filename|100 col 10-15|message
        " want-> filename, 100, message
        let resource = deepcopy(self.res)->map({_,v
                    \ ->split(v,'|')[0].': '
                    \ .split(split(v,'|')[1], ' ')[0].': '
                    \ .join(split(v,'|')[2:], '')})
    endif

    let &errorformat = ef
    cgetexpr resource | cw
    let &errorformat = tmp
    cal s:fzsearch.finalize()
endf

" finalize
fu! s:fzsearch.finalize() abort
    let s:fzsearch.list = []
    let s:fzsearch.res = []
    cal s:runcat.stop()
    cal popup_clear()
endf

" preview scroll
fu! s:fzsearch.scroll(wid, vector) abort
    if self.tid
        retu
    endif
    cal timer_stop(self.tid)
    let vec = a:vector ? "\<C-n>" : "\<C-p>"
    let self.tid = timer_start(10, { -> popup_filter_menu(a:wid, vec) }, #{repeat: -1})
    let delay = 600
    cal timer_start(delay, self.scstop)
endf

fu! s:fzsearch.scstop(_) abort
    cal timer_stop(self.tid)
    let self.tid = 0
endf

" preview draw update
fu! s:fzsearch.pvupd() abort
    let win = winbufnr(self.pwid)

    if empty(self.res) && self.type == 'lm'
        retu
    endif
    if self.type == 'lm'
        let lnm = split(self.res[self.ridx], ':')[0]
        cal popup_setoptions(self.pwid, #{firstline: (lnm-10 > 0 ? lnm-10 : 1)})
        cal win_execute(self.pwid, 'exe '.lnm)
        retu
    endif

    " del
    sil! cal deletebufline(win, 1, getbufinfo(win)[0].linecount)
    " put
    if empty(self.res)
        retu
    endif
    let path = self.type == 'flm'
                \ ? split(self.res[self.ridx], '|')[0]
                \ : substitute(self.res[self.ridx], '\~', $HOME, 'g')
    let read = ['Cannot open file.', 'please check this file path.', path]
    try
        let read = readfile(path)
    catch
    endtry
    cal setbufline(win, 1, read)
    " syntax
    let ext = matchstr(path, '[^\.]\+$')
    let ext = ext !~ '^[a-zA-Z0-9]\+$' ? '' : ext
    let ext = get(self.ffdict, ext, ext)
    cal setbufvar(win, '&filetype', ext)
    " line
    let sep = split(self.res[self.ridx], '|')
    let lnm = len(sep) < 2 ? 1 : split(sep[1], ' ')[0]
    cal popup_setoptions(self.pwid, #{firstline: (lnm-10 > 0 ? lnm-10 : 1)})
    cal win_execute(self.pwid, 'exe '.lnm)
endf

" search result update
fu! s:fzsearch.list_upd() abort
    " upd match result list
    let filterd = empty(self.wd) ? self.list :
                \ self.mode == 'Fuzzy' ? matchfuzzy(self.list, self.wd)
                \ : copy(self.list)->filter({_,v->match(v, self.wd)!=-1})
    ""let filterd = copy(self.list)->filter({_,v->match(v, self.wd)!=-1})
    let self.res = filterd[0:self.max]
    let self.ridx = len(self.res)-1 < self.ridx ? len(self.res)-1 : self.ridx
    cal setbufline(winbufnr(self.ewid), 1, self.pr . self.wd)
    cal deletebufline(winbufnr(self.rwid), 1, self.max)
    echo ''
    cal setbufline(winbufnr(self.rwid), 1, self.res)
    " highlight match char
    cal clearmatches(self.rwid)
    if !empty(self.wd)
        cal matchadd('FzMatch', self.mode == 'Fuzzy'
                    \ ? printf('\c[%s]', escape(self.wd, '\[\]\-\.\*'))
                    \ : '\c'.self.wd,
                    \ 16, -1, #{window: self.rwid})
    endif
    " upd preview
    cal self.pvupd()
endf

fu! s:fzsearch.togglemode() abort
    let self.mode = self.mode == 'Fuzzy' ? 'Simply' : 'Fuzzy'
    let self.pr = '['.self.mode.']'.(self.prr ?? '>>')
    cal clearmatches(self.ewid)
    cal matchaddpos('FzEWin', [[1, 1, len(self.pr)]],  16, -1, #{window: self.ewid})
    cal matchadd('DarkRed', '\[.*\]', 17, -1, #{window: self.ewid})
    cal self.list_upd()
endf

" enter search word
fu! s:fzsearch.fil(ctx, wid, key) abort
    if a:key is# "\<Esc>"
        cal popup_close(self.bwid)
        cal popup_close(self.pwid)
        cal popup_close(self.ewid)
        cal feedkeys("\<C-c>")
        retu 1
    elseif a:key is# "\<C-n>" || a:key is# "\<C-p>" || a:key is# "\<CR>"
        " only largest zindex window is active.
        cal popup_setoptions(self.rwid, #{zindex: 100})
        cal popup_setoptions(self.ewid, #{zindex: 99})
        cal popup_setoptions(self.pwid, #{zindex: 98})
        cal feedkeys(a:key)
        retu 1
    elseif a:key is# "\<C-d>" || a:key is# "\<C-u>"
        cal popup_setoptions(self.pwid, #{zindex: 100})
        cal popup_setoptions(self.ewid, #{zindex: 99})
        cal popup_setoptions(self.rwid, #{zindex: 98})
        cal feedkeys(a:key)
        retu 1
    elseif a:key is# "\<Tab>"
        cal self.togglemode()
        retu 1
    elseif a:key is# "\<C-q>"
        cal self.quickfix()
        cal feedkeys("\<Esc>")
        retu 1
    " TODO fzf.vim copy shold D-v only or Shift Insert. C-v want split
    " TODO fzf.vim add feature: refresh cache (or re search)
    elseif a:key is# "\<C-v>" || a:key is# "\<D-v>"
        for i in range(0, strlen(@+)-1)
            cal add(self.wda, strpart(@+, i, 1))
        endfor
    " TODO see self
    elseif a:key is# "\<BS>" && !empty(self.wda)
        unlet self.wda[len(self.wda)-1]
    elseif a:key is# "\<BS>" && len(self.wda) == 0
        " noop
        retu 1
    elseif a:key is# "\<C-w>"
        let self.wda = []
    elseif strtrans(a:key) == "<80><fd>`"
        " noop (for polyglot bug adhoc)
        retu 1
    else
        cal add(self.wda, a:key)
    endif

    let self.wd = join(self.wda, '')
    cal self.list_upd()
    retu a:key is# "x" || a:key is# "\<Space>" ? 1 : popup_filter_menu(a:wid, a:key)
endf

" choose result
fu! s:fzsearch.jk(_, wid, key) abort
    if a:key is# "\<Esc>"
        cal popup_close(self.bwid)
        cal popup_close(self.pwid)
        cal popup_close(self.ewid)
        cal feedkeys("\<C-c>")
    elseif a:key is# "\<C-n>"
        let self.ridx = self.ridx == len(self.res)-1 ? len(self.res)-1 : self.ridx+1
        cal self.pvupd()
        retu popup_filter_menu(a:wid, a:key)
    elseif a:key is# "\<C-p>"
        let self.ridx = self.ridx ? self.ridx-1 : 0
        cal self.pvupd()
        retu popup_filter_menu(a:wid, a:key)
    elseif a:key is# "\<CR>"
        cal popup_close(self.ewid)
        cal popup_close(self.pwid)
        cal popup_close(self.bwid)
        retu popup_filter_menu(a:wid, empty(self.res) ? "\<C-c>" : a:key)
    elseif a:key is# "\<C-d>" || a:key is# "\<C-u>"
        cal popup_setoptions(self.pwid, #{zindex: 100})
        cal popup_setoptions(self.ewid, #{zindex: 99})
        cal popup_setoptions(self.rwid, #{zindex: 98})
        cal feedkeys(a:key)
    else
        cal popup_setoptions(self.ewid, #{zindex: 100})
        cal popup_setoptions(self.rwid, #{zindex: 99})
        cal popup_setoptions(self.pwid, #{zindex: 98})
        cal feedkeys(a:key)
    endif
    retu 1
endf

" scroll preview
fu! s:fzsearch.pv(_, wid, key) abort
    if a:key is# "\<Esc>"
        cal popup_close(self.bwid)
        cal popup_close(self.pwid)
        cal popup_close(self.ewid)
        cal feedkeys("\<C-c>")
    elseif a:key is# "\<C-d>"
        cal self.scroll(a:wid, 1)
    elseif a:key is# "\<C-u>"
        cal self.scroll(a:wid, 0)
    elseif a:key is# "\<C-n>" || a:key is# "\<C-p>" || a:key is# "\<CR>"
        cal popup_setoptions(self.rwid, #{zindex: 100})
        cal popup_setoptions(self.ewid, #{zindex: 99})
        cal popup_setoptions(self.pwid, #{zindex: 98})
        cal feedkeys(a:key)
    else
        cal popup_setoptions(self.ewid, #{zindex: 100})
        cal popup_setoptions(self.rwid, #{zindex: 99})
        cal popup_setoptions(self.pwid, #{zindex: 98})
        cal feedkeys(a:key)
    endif
    retu 1
endf

" =====================
fu! s:fzf_histories()
    cal s:fzsearch.popup(#{title: 'Histories', list: execute('ol')->split('\n')->map({_,v -> split(v, ': ')[1]}),
        \ type: 'f', eprfx: '['.substitute(getcwd(), $HOME, '~', 'g').']>>'})
endf

fu! s:fzf_buffers()
    cal s:fzsearch.popup(#{title: 'Buffers', list: execute('ls')->split('\n')->map({_,v -> split(v, '"')[1]})
            \ ->filter({_,v -> v != '[No Name]' && v != '[無名]' && v !~ '!.*'}),
        \ type: 'f', eprfx: '['.substitute(getcwd(), $HOME, '~', 'g').']>>'})
endf
" =====================

aug FzColor
    au!
    au ColorScheme * hi FzMatch cterm=BOLD cterm=underline ctermfg=196 ctermbg=237
    au ColorScheme * hi FzWin ctermfg=114 ctermbg=237
    au ColorScheme * hi FzBWin cterm=BOLD ctermfg=145 ctermbg=238
    au ColorScheme * hi FzEWin ctermfg=39 ctermbg=237
aug END

noremap <silent><Plug>(fzf-histories) :<C-u>cal <SID>fzf_histories()<CR>
noremap <silent><Plug>(fzf-buffers) :<C-u>cal <SID>fzf_buffers()<CR>
" }}}

```


</div></details>



## 使い方
検索にはSimpleモードとFuzzyモードがあり
Simpleの場合は入力文字そのままを、Fuzzyの場合はあいまい検索します。

Gitのls-filesやヒストリー、バッファリスト、バッファ内文字列やQuickFix内など
とにかく文字列のリストを渡せばこのポップアップで検索、プレビューできます。
（batコマンドがなくとも色がつきます。vimのバッファをただ開いているだけだから。）
![fzsearch1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453310/43f5f030-a2b8-c301-2bd0-c3fb528925be.gif)

検索結果をQuickFixに渡し、取っておくことも可能。ポップアップの弱点を解消してます。
また、QuickFix内をポップアップで検索してQuickFixにもどしてt、無限ループもできます。
![fzsearch2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453310/c9c58af2-88d1-c66e-caaa-43c6859105be.gif)

### バッファ内、もしくはQuickFixのバッファ内を検索する
```vim
" Fuzzy search current file {{{
fu! s:currentSearch() abort
    let buf = getline(1, line('$'))
    " quickfix currentSearch
    " empty buffer -> no data
    if expand('%')->empty()
        cal s:fzsearch.popup(#{title: 'QuickFix', list: buf, type: 'flm', eprfx: 0})
        retu
    endif
    " buffer currentSearch
    cal s:fzsearch.popup(#{title: 'Current', list: map(buf, {i,v->i+1.': '.v}), type: 'lm', eprfx: 0})
endf

noremap <silent><Plug>(current-search) :<C-u>cal <SID>currentSearch()<CR>
" }}}
```
### ヒストリーを検索
```vim
fu! s:fzf_histories()
    cal s:fzsearch.popup(#{title: 'Histories', list: execute('ol')->split('\n')->map({_,v -> split(v, ': ')[1]}),
        \ type: 'f', eprfx: '['.substitute(getcwd(), $HOME, '~', 'g').']>>'})
endf
```
### バッファを検索
```vim
fu! s:fzf_buffers()
    cal s:fzsearch.popup(#{title: 'Buffers', list: execute('ls')->split('\n')->map({_,v -> split(v, '"')[1]})
            \ ->filter({_,v -> v != '[No Name]' && v != '[無名]' && v !~ '!.*'}),
        \ type: 'f', eprfx: '['.substitute(getcwd(), $HOME, '~', 'g').']>>'})
endf
```

### ファイル、Git ls-filesを検索
これには工夫が必要。（ちょっとTODO残ってますが。。。）
gitのリポジトリフォルダである場合は`git ls-files`結果を渡せばいいですが
そうでない場合は、つまり[fzfコマンドをやりたい場合](https://github.com/junegunn/fzf.vim)は難しいです。

今回はどうしてもfindコマンドでやりたかったので（プラグイン不要だから）、
最下層までの検索をあきらめ、1階層ずつ非同期プロセスで検索し、結果を逐次渡してます。
gitプロジェクト以外で使わなきゃいいだけの話。
```vim
let s:fzf = #{cache: [], maxdepth: 4, gcache: [],
    \ not_path_arr: [
         \'"*/.**/*"',
         \'"*node_modules/*"', '"*target/*"'
         \'"*Applications/*"', '"*AppData/*"', '"*Library/*"',
         \'"*Music/*"', '"*Pictures/*"', '"*Movies/*"', '"*Videos/*"'
         \'"*OneDrive/*"',
    \ ],
\}

" TODO fzf delete
fu! TestFzfMaxDepth(n) abort
    let s:fzf.maxdepth = a:n
endf
" TODO fzf maxdepth かえるコマンド作るか

let s:fzf.postfix = ' -type f -not -path '.join(s:fzf.not_path_arr, ' -not -path ')
let s:fzf.searched = getcwd()
let s:fzf.get_gitls = {-> system('git ls-files -c')->split('\n')->filter({_,v->!empty(v)}) }
let s:fzf.get_file_d1 = {-> system('find . -mindepth 1 -maxdepth 1'.s:fzf.postfix)->split('\n')->filter({_,v->!empty(v)}) }
let s:fzf.get_file = { v -> 'find . -mindepth '.v.' -maxdepth '.v.s:fzf.postfix }

fu! s:fzf.files() abort
    let pwd = getcwd()
    let chk = system('git status')
    let self.is_git = v:shell_error ? 0 : 1

    " moved or first
    if stridx(pwd, self.searched) == -1
                "\ || (self.is_git && empty(self.gcache))
                \ || (self.is_git)
                \ || (!self.is_git && empty(self.cache))
        let self.searched = pwd

        if self.is_git
            " TODO fzf no need cache?
            let self.gcache = self.get_gitls()
            cal s:echoI('cache: git ls-files -c')
        else
            let self.cache = self.get_file_d1()
            cal self.asyncfind()
        endif
    endif

    cal s:fzsearch.popup(#{title: self.is_git ? 'Project Files' : 'Files',
        \ list: self.is_git ? self.gcache : self.cache,
        \ type: 'f', eprfx: '['.substitute(pwd, $HOME, '~', 'g').']>>'})
endf

fu! s:fzf.asyncfind() abort
    cal s:runcat.start()
    let self.notwid = popup_notification('find files in ['.s:fzf.searched.'] and caching ...', #{zindex: 51, line: &lines, col: 5})
    let self.jobcnt = self.maxdepth-1
    let self.endjobcnt = 0
    for depth in range(2, self.maxdepth)
        cal job_start(self.get_file(depth), #{out_cb: self.asyncfind_start, close_cb: self.asyncfind_end})
    endfor
endf

fu! s:fzf.asyncfind_start(ch, msg) abort
    cal add(self.cache, a:msg)
endf

fu! s:fzf.asyncfind_end(ch) abort
    let s:fzsearch.list = self.cache
    cal s:fzsearch.list_upd()

    let self.endjobcnt += 1
    if self.endjobcnt == self.jobcnt
        cal s:runcat.stop()
        cal popup_close(self.notwid)
        cal popup_notification('find files cached !', #{zindex: 51, line: &lines, col: 5})
    endif
endf

fu! s:fzfexe() abort
    cal s:fzf.files()
endf
noremap <silent><Plug>(fzf-smartfiles) :<C-u>cal <SID>fzfexe()<CR>
```

## 仕組み
- ポップアップを4枚つくる（1枚は下敷き）
- 入力文字を描画し、検索結果を絞り、ヒット文字を赤くする
- 入力キーに応じて操作ポップアップを変えるため、ポップアップのzindexを変える
  (C-n/pで検索結果ウィンドウ、C-d/uでプレビューウィンドウ、その他全てで入力ウィンドウ)
- 検索対象がファイルなのか、行に飛ぶ物なのか（マーク一覧、バッファ内）で分岐
- ファイル検索の場合、ファイル名の拡張子に応じてシンタックスハイライトをプレビューに
- 行検索の場合、検索結果とプレビュー両方にシンタックスハイライト
- 決定されたらジャンプ。これもファイルなのか行なのかでコマンドを変える

ざっくり説明ですがこんな感じです。
QuickFixに対応するため、検索結果リストは`'%f: %l: %m'`のフォーマットで
`ファイル名:行番号:テキスト`のリストとなるように統一しました。