Vim Advent Calendar 2012 の 4 日目の記事です。
- 「やった!コンプガチャで Vim 出た!」
- 「だって前の彼氏、Vim 使いじゃなかったんだもん」
- 「マクドナルド店員「ご一緒に Vim など如何ですか?」」
こんな言葉が聞かれる様になって随分と経ちました(要出典)。
昨今、Vim はテキストエディタの枠を超え、アプリケーションプラットフォームへと変わりつつあります。
vital.vim 等を使う事で簡単にアプリケーションを作る事も出来る様になりました。手前味噌ではありますが webapi-vim の一部も vital.vim に取り込まれています。
このブログでも結構取り上げていますが webapi-vim とは一体何か。名前の通り、Web Application Programming Intreface を扱えるライブラリです。
webapi-vim とは
webapi-vim を使えば例えばこんな事が出来ます。
HTTP で GET
let res = webapi#http#get("http://example.com")
res には以下の様な構造が返ります。
{
"header": [
"Content-Type: text/html",
"Content-Length: 310"
],
"content": "<html> ....."
}
header にはヘッダの配列、content には受信したデータが戻ります。
引数にはパラメータを渡せます。
let res = webapi#http#get("http://google.com", { "q": "vim" })
HTTP で POST
let res = webapi#http#post("http://google.com", { "q": "vim" })
GET とほぼ同じインタフェースです。GET も同様ですが、第三引数 header を指定出来ます。
XML のパース
let dom = webapi#xml#parse('<vim><vimer id="1">bram</vimmer></vim>')
DOM オブジェクトが返ります。以下の様な構造になっています。
{
"name": "vim",
"attr": {},
"child": [
{
"name": "vimmer",
"attr": { "id": 1 },
"child": [
"bram"
]
}
]
}
dom からは以下の様に探索出来ます。
dom.childNode("vimmer")
これで子ノードのうち vimmer ノードが1つだけ返ります。
dom.childNode("vimmer", {"id": 1})
vimmer ノードのうち id 属性が 1 の物が1つだけ返ります。
dom.childNodes("vimmer", {"id": 1})
vimmer ノードで id 属性が 1 の物が全て返ります。childNode/childNodes は子ノードだけですが、再帰的に検索する場合 find/findAll を使います。
node.value()
ノードをテキスト化します。上記で得た bram が入った vimmer ノードで value() を実行すると "bram" という文字列が得られます。
node.toString()
ノードをXML化します。なお、属性はノードに対して attr というフィールドからディクショナリで参照出来ます。
echo node.attr["id"]
HTML のパース
let dom = webapi#html#parse(...)
扱い方は XML と同じです。
JSON のエンコード/デコード
let obj = webapi#json#decode(json)
vim の値として扱えます。逆に
let json = webapi#json#encode(obj)
でエンコード出来ます。
webapi-vim にはこれ以外にも XMLRPC/JSONRPC/SOAP といったRPC、MD5/SHA1 といったハッシュ関数、OAuth もあります。詳しくはドキュメントを見て頂くか、サンプルや webaip-vim を使ったプロダクトのソースコードを参照して下さい。
metarw とは
さて、いまごろ本題
昨今のモテる Vim 使いはどんな物に対しても vim から読み書きを行います(要出典)。その代表例として vim-metarw という物もあります。
kana/vim-metarw - GitHub
Vim plugin: A framework to read/write fake:path
https://github.com/kana/vim-metarw
これは kana さんが作ったフレームワークで、metarw 自身は vim からメタ情報を読み書きする土台のみを提供します。各 metarw プラグインは metarw のルールに従って実装コードを入れる事で vim と親和性の高いファイルの読み書きが出来るという物です。例えば vim-metarw-gist という物があります。
emonkak/vim-metarw-gist - GitHub
Vim plugin: metarw scheme for gist
https://github.com/emonkak/vim-metarw-gist
これを使うと
:e gist:
で vim が持っているファイルエクスプローラと同じ見栄えで自分の gist 一覧が表示され、エンターを押すと gist のファイルが開かれ、
:w
で書き込めます。
なお Windows で metarw を使う場合には私の fork を使って下さい。
mattn/vim-metarw - GitHub
https://github.com/mattn/vim-metarw
ここで metarw プラグインについて説明しましょう。
vim-metarw プラグインを作る場合はまずプラグインフォルダを用意し、
autoload/metarw/プラグイン名.vim
というファイルを作ります。
vim-metarw プラグインを満たす為には以下の実装を含んでいる必要があります。
metarw#gdrive#read(fakepath)
metarw#gdrive#write(fakepath, line1, line2, append_p)
metarw#gdrive#complete(arglead, cmdline, cursorpos)
まず read を説明します。metarw プラグインは fakepath で指定されたパスがディレクトリかどうかを判断し、ディレクトリであれば
['browse', [
{"label": "ふー", "fakepath": "xxx:/foo"},
{"label": "ばー", "fakepath": "xxx:/bar"}
]]
browse という結果と共にパス情報を返します。エラーが発生した場合には
['error', 'エラーメッセージ']
を返します。ファイルであった場合には、既に metarw が用意したバッファにコンテンツを貼り付け、必要であれば filetype も変更して
['done', '']
という結果を返します。write も同様に、fakepath で指定されたパスに対して現在のバッファの line1 行から line2 行のコンテンツを書き込みます。append_p の場合は追記になります。
最後に complete は vim の補完と同じ仕組みになります。実際には、read の際にファイル一覧を作った結果の fakepath 部分を返せば良い事になります。
ここまで来れば誰でも metarw プラグインを作れる様になります。
vim-metarw-gdrive でハードウェア境界を越えろ
上記で説明した webapi-vim と、この vim-metarw を使い、
Google Drive のファイルを読み書き出来る物を作ってみましょう。
なっ!?なんだってーーー!
まずは read で指定されたパスから属性情報を取り出す部分を作ります。
function! s:parse_incomplete_fakepath(incomplete_fakepath)
let _ = {}
let fragments = split(a:incomplete_fakepath, '^\l\+\zs:', !0)
if len(fragments) <= 1
echoerr 'Unexpected a:incomplete_fakepath:' string(a:incomplete_fakepath)
throw 'metarw:gdrive#e1'
endif
let _.given_fakepath = a:incomplete_fakepath
let _.scheme = fragments[0]
let _.path = fragments[1]
if fragments[1] == '' || fragments[1] =~ '^[\/]$'
let _.id = 'root'
else
let _.id = split(fragments[1], '[\/]')[-1]
endif
return _
endfunction
Google Drive SDK の API リファレンスによると、Google Drive の API では各ファイルにパスでアクセスする事は出来ません。パスから特定のコンテンツを得たり保存を行う場合には、ノードIDに対して割り当てられた名称を取得し、例えば
/foo/bar/baz.txt
というパスのコンテンツを得る場合、実は bar というフォルダのIDだけ分かれば書き込める事になります。ただしラベルは同じノード内においても重複し得ます。なのでパス
/foo/bar/baz.txt
という情報から目的の
baz.txt
のコンテンツを得るには
- "root" ノード直下のファイル一覧を調べ、ディレクトリかつラベルが foo の最初の物を探す
- foo 直下のファイル一覧を調べ、ディレクトリかつラベルが bar の最初の物を探す
- bar 直下のファイル一覧を調べ、ファイルかつラベルが baz.txt の最初の物を探す
- baz.txt のコンテンツをダウンロードする
こいうめんどくさい手順を取る必要があります。たかがファイルを読みたいだけに4回 API アクセスが必要になります。どう考えてもおかし過ぎるし使っててイライラするだろうから、このプラグインで扱うパス情報は ID で行う事とし、ファイルブラウザで実際のファイル名からアクセスしてもらう事とします。
function! metarw#gdrive#read(fakepath)
let _ = s:parse_incomplete_fakepath(a:fakepath)
if _.path == '' || _.path =~ '[\/]$'
let result = s:read_list(_)
else
let result = s:read_content(_)
endif
return result
endfunction
まずは先ほど作った parse_incomplete_fakepath に従い、ファイル一覧を読むのかファイルのコンテンツを読むのかを切り分けます。
function! s:read_list(_)
call s:load_settings()
let result = []
let res = webapi#json#decode(webapi#http#get('https://www.googleapis.com/drive/v2/files', {'access_token': s:settings['access_token'], 'q': printf("'%s' in parents", a:_.id)}).content)
if has_key(res, 'error')
return ['error', res.error.message]
endif
for item in res.items
if item.labels.trashed != 0
continue
endif
let title = item.title
let file = item.id
if item.mimeType == 'application/vnd.google-apps.folder'
let title .= '/'
let file .= '/'
endif
if len(a:_.path) == 0
let file = '/' . file
else
let file = a:_.path . file
endif
call add(result, {
\ 'label': title,
\ 'fakepath': printf('%s:%s', a:_.scheme, file)
\ })
endfor
return ['browse', result]
endfunction
Google Drive API を使って、ノードID に属するファイルの一覧を取得しています。
function! s:read_content(_)
call s:load_settings()
let res = webapi#json#decode(webapi#http#get('https://www.googleapis.com/drive/v2/files/' . webapi#http#encodeURI(a:_.id), {'access_token': s:settings['access_token']}).content)
if has_key(res, 'error')
return ['error', res.error.message]
endif
if !has_key(res, 'downloadUrl')
return ['error', 'This file seems impossible to edit in vim!']
endif
let resp = webapi#http#get(res.downloadUrl, '', {'Authorization': 'Bearer ' . s:settings['access_token']})
if resp.header[0] !~ '200'
return ['error', resp.header[0]]
endif
let content = resp.content
call setline(2, split(iconv(content, 'utf-8', &encoding), "\n"))
let ext = '.' . res.fileExtension
if has_key(s:extmap, ext)
let &filetype = s:extmap[ext]
endif
return ['done', '']
endfunction
ノードIDからコンテンツをダウンロードしています。ファイル名に拡張子がついていないので、拡張子からファイルタイプを決定する為にローカルにテーブルを保持しました。
ここで metarw を使う際の注意点があります。metarw の read 関数実装は
:r! ...
といった外部ファイルを読み込む事を想定しており、vim のデフォルト動作と同様に先頭行に空行が入れる必要があります。この空行は metarw 本体側で削除されるので、metarw プラグイン側が空白行を入れる必要があります。
write も同様に API から行います。
function! s:write_content(_, content)
call s:load_settings()
let res = webapi#json#decode(webapi#http#post('https://www.googleapis.com/upload/drive/v2/files/' . webapi#http#encodeURI(a:_.id), a:content, {'Authorization': 'Bearer ' . s:settings['access_token'], 'Content-Type': 'application/octet-stream'}, 'PUT').content)
if has_key(res, 'error')
return ['error', res.error.message]
endif
return ['done', '']
endfunction
function! metarw#gdrive#write(fakepath, line1, line2, append_p)
let _ = s:parse_incomplete_fakepath(a:fakepath)
if _.path == '' || _.path =~ '[\/]$'
echoerr 'Unexpected a:incomplete_fakepath:' string(a:incomplete_fakepath)
throw 'metarw:gdrive#e1'
else
let content = iconv(join(getline(a:line1, a:line2), "\n"), &encoding, 'utf-8')
let result = s:write_content(_, content)
endif
return result
endfunction
このプラグインは Google Drive に oauth2 で認証を行っています。初めて使う際には
:GdriveSetup
を実行し、ブラウザで表示されたコードを vim の
CODE:
部にコピペして頂く必要があります。
Google Drive の API、というか最近の Google の API は認証情報に揮発性があり、リフレッシュトークンという物を使って有効期限が切れたアクセストークンを再生成する必要があります。上記のコードには含まれませんがリポジトリでは401による再認証が行われます。
それでもエラーが出る場合は、お手数ですが再度
:GdriveSetup
を実行して認証を行って下さい。
ソースを github に置いてあります。興味のある方は覗いてみて下さい。
mattn/vim-metarw-gdrive - GitHub
https://github.com/mattn/vim-metarw-gdrive
全て webapi-vim もそうですが vim スクリプトだけで組まれています。(ただしネットワーク通信部分だけは curl もしくは wget を使っています)
さて実際に使ってみましょう。上記の認証を終わらせた後
:e gdrive:
を実行します。
ファイル一覧が表示されました。
なんと都合よく「helloworld.cxx」なんてファイルがあるではありませんか!!!
エンターキーを押します。
おぉぉ...
もちろん
:w
で保存する事も出来ますし、quickrun から直で実行する事も出来ます。
ファイルを作成する場合は、ファイルブラウザに表示されるパスを使って「:w gdrive:/XXXXXXXX/foo.txt」と実行して下さい。現在のバッファが指定のフォルダにアップロードされます。この場合、バッファは既にテンポラリに過ぎないので更新したい場合はファイルブラウザから開きなおして下さい。
まとめ
まとめると...
モテる Vimmer に読み書き出来ないファイルなど無かったんだよ!!
ってことです。
追記
モテる度合いには個人差がございます。