Help us understand the problem. What is going on with this article?

popup windowsで簡易ファイラーを作る

ども、ゴリラです。
今年もやってまいりましたVimアドベントカレンダーです。

今日はpopup windowを使ってファイル一覧をpopup windowで選択して開くプラグインを作ってみようと思います。
プラグイン作ってみたい方は、ぜひ読んでみてください。意外とプラグインは簡単に作れます。

はじめに

まずどんな機能を作るかを整理します。本記事は次の機能を実装することをゴールにします。

  1. 指定パスのファイル一覧をpopup windowに表示
  2. 選択したファイルを開く(分割、タブ、カレントバッファ)

ディレクトリ構成

プラグインのディレクトリ構成は以下にします。一般的にautoload配下にメインの処理関数を定義して、
pluginでメイン処理の関数を呼び出すコマンドを定義します。

autoloadに定義した関数は、コマンドで呼び出されるまでロードされないようになっています。
これは、Vimの起動時間に影響を与えないようにするためです。

popupfiles.vim/
├── autoload
│   └── popupfiles.vim
└── plugin
    └── popupfiles.vim

ディレクトリの置き場

プラグインマネージャーを使う方が多くいると思います、プラグイン開発を行うとき筆者はよくpackages機能を使用します。
packpath配下にstartディレクトリを作成してそこにディレクトリを置くことで、Vim起動時にプラグインを読み込んでくれます。

ちなみに、筆者の場合は次のようになっています。

/Users/skanehira/.vim/pack/plugins/start/popupfiles.vim

詳しく知りたい方は:h packagesでヘルプを引いてみてください。

ファイル一覧をpopup windowに表示

ファイル一覧を取得

まずはカレントディレクトリ配下のファイルを一覧の配列を返す関数を用意します。
コードはautoload/popupfiles.vimに記述します。

" パスのセパレーターを取得
let s:sep = fnamemodify('.', ':p')[-1:]

function! s:get_files(path) abort
    " `expand()`はパスをフルパスに展開
    " 例えば`~/`は`/home/gorilla`に展開
    let p = expand(a:path)
    let entries = []

    " `readdir`は指定したディレクトリ内の要素を配列で返す
    for f in readdir(p)
        " `.`から始まるディレクトリとファイルをスキップ
        if f[0] is# '.'
            continue
        endif

        let file_path = p . s:sep . f
        if isdirectory(file_path)
            let entries += s:get_files(file_path)
        else
            if file_path[0] is# '.'
                let file_path = file_path[2:]
            endif
            call add(entries, file_path)
        endif
    endfor

    return entries
endfunction

popup windowを作る

次にpopup windowを作る関数を定義します。popup windowを作る関数はいくつかありますが、今回はpopup_menuを使用します。popup_menuを使用することで要素の選択、操作を簡単に実装できます。

こちらも、コードはautoload/popupfiles.vimに記述します。

function! popupfiles#files() abort
    call popup_menu(s:get_files('.'), {})
endfunction

ここで1つポイントです。popupfiles#files()という関数名はpluginで呼び出すためにautoloadのルールに従って命名しています。
autoloadの命名ルールはfilename#funcname()になっています。こうすることで、どのファイルのどの関数を呼び出すかVim側が判断できるようになるからです。

popup windowを呼び出す

メインの処理ができたので、次にpluginpopupfiles#files()を呼び出すコマンドを次のように定義します。

" プラグインを再度ロードしないようにガード
if exists('g:loaded_popupfiles')
  finish
endif

let g:loaded_popupfiles = 1

" vi互換の設定を一旦退避
let s:save_cpo = &cpo
set cpo&vim

" コマンドの定義
command! PopupFiles call popupfiles#files()

" 互換設定を戻す
let &cpo = s:save_cpo
unlet s:save_cpo

一度Vimを再起動するか、:source plugin/popupfiles.vim:source autoload/popupfiles.vimでファイルを読み込むかしてください。

適用後:PopupFilesコマンドを実行すると次のようにpopup windowにファイル一覧が表示されるはずです。

image.png

これでpopup windowにファイル一覧を表示させることができました。
ちなみに、popup_menuではデフォルトで次のキーが利用できます。

キー 説明
j 次の要素
k 前の要素
enter/ space 要素を選択
x / esc / ctrl-c キャンセル

では、次に選択したファイルを開くキーバインドを追加していきましょう。

選択したファイルを開く

popup_menuでは第1引数に要素、第2引数にオプションを渡すことができます。
もう一度コードを見てみましょう。第2引数は{}は空のオブジェクトになっています。
このオブジェクトにオプションを定義していきます。

function! popupfiles#files(path) abort
    call popup_menu(s:get_files(a:path), {})
endfunction

popup windowにはfilterというオプションがあります。
このオプションではキー入力を受け付けて何かしらの処理を行う関数を指定することができます。

このフィルター関数、デフォルトではwinidkeyの2つの引数を受け取ります。
フィルター関数の定義と呼び出しは次のようになります。

function! popupfiles#files() abort
    call popup_menu(s:get_files('.'), {
                \ 'filter': function('s:popup_files_filter')
                \ })
endfunction

function! s:popup_files_filter(winid, key) abort
    " 入力キーに応じた処理

    " それ以外は通常のpopup_filter_menuに渡す
    return popup_filter_menu(a:winid, a:key)
endfunction

ここで1つポイントですが、function()で関数への参照を取得して、filterオプションに渡す必要があります。

また、popup_filter_menuというVimの組み込み関数がありますが、これはfilterが何も指定されないときにデフォルトで使用されるフィルター関数になっています。この関数を使用することでjkといったキーバインドの操作が出来るようになります。

しかし、このまま実装しようとすると、
フィルター関数に選択したファイルが渡ってこないため、ファイルを特定できないという問題があります。
そこで、Vim scriptのPartialという仕組みを使用して必要な情報をフィルター関数の引数に追加します。
function()の第2引数に配列を渡すことで、その中身が関数の引数として先頭に順に追加されます。

function! popupfiles#files() abort
    let files = s:get_files('.')
    let ctx = {
                \ 'idx': 0,
                \ 'files': files,
                \ }

    call popup_menu(files, {
                \ 'filter': function('s:popup_files_filter', [ctx])
                \ })
endfunction

function! s:popup_files_filter(ctx, winid, key) abort
    " 入力キーに応じた処理

    " それ以外は通常のpopup_filter_menuに渡す
    return popup_filter_menu(a:winid, a:key)
endfunction

idxfilesから選択したファイルを取り出すために必要な配列番地の情報です。

これで必要な情報をフィルター関数内で扱うことができるので、あとはキー入力に応じてファイルを開く処理を実装すれば良いです。

今回は、次のキーバインドを実装することにします。

キー 説明
enter カレントバッファに開く
ctrl-v 水平分割で開く
ctrl-x 垂直分割で開く
ctrl-t タブページで開く

実装したコードが次になります。

function! s:popup_files_filter(ctx, winid, key) abort
    " 入力キーに応じた処理
    if a:key is# 'j'
        if a:ctx.idx < len(a:ctx.files) -1
            let a:ctx.idx = a:ctx.idx + 1
        endif
    elseif a:key is# 'k'
        if a:ctx.idx > 0
            let a:ctx.idx = a:ctx.idx - 1
        endif
    elseif a:key is# "\<cr>"
        return s:open_file(a:winid, 'e', a:ctx.files[a:ctx.idx])
    elseif a:key is# "\<c-v>"
        return s:open_file(a:winid, 'vnew', a:ctx.files[a:ctx.idx])
    elseif a:key is# "\<c-x>"
        return s:open_file(a:winid, 'new', a:ctx.files[a:ctx.idx])
    elseif a:key is# "\<c-t>"
        return s:open_file(a:winid, 'tabnew', a:ctx.files[a:ctx.idx])
    endif

    " それ以外は通常のpopup_filter_menuに渡す
    return popup_filter_menu(a:winid, a:key)
endfunction

function! s:open_file(winid, open, file) abort
    call popup_close(a:winid)
    execute a:open a:file
    return 1
endfunction

これで簡易ファイラーの実装は以上になります。

まとめ

いかがだったでしょうか?合計で100行くらいで簡易ファイラーを実装できるVim script、便利ですね。
これをきっかけにプラグインを作ってみようってなってくれたら嬉しいなと思っています。

本記事のソースコードはリポジトリに置きましたので、全体像を掴めていない方は覗いてみてください。

https://github.com/skanehira/popupfiles.vim

なお、本記事はVim scriptの基本構文について触れていないので、詳しく知りたい方は:h scriptでヘルプを参照してください。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away