2014/02/04

Recent entries from same category

  1. VimConf 2023 Tiny に参加しました
  2. Vim で Go 言語を書くために行った引越し作業 2020年度版
  3. Vim をモダンな IDE に変える LSP の設定
  4. ぼくがかんがえたさいきょうの Vim のこうせい 2019年 年末版
  5. VimConf 2019 を終えて

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 で指定されたパスから属性情報を取り出す部分を作ります。 functions: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 に従い、ファイル一覧を読むのかファイルのコンテンツを読むのかを切り分けます。
functions: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 に属するファイルの一覧を取得しています。
functions: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(2split(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 から行います。 functions: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:line1a: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: を実行します。
vim-metarw-gdrive1
ファイル一覧が表示されました。
なんと都合よく「helloworld.cxx」なんてファイルがあるではありませんか!!!
エンターキーを押します。

vim-metarw-gdrive2
おぉぉ...

もちろん :w で保存する事も出来ますし、quickrun から直で実行する事も出来ます。
ファイルを作成する場合は、ファイルブラウザに表示されるパスを使って「:w gdrive:/XXXXXXXX/foo.txt」と実行して下さい。現在のバッファが指定のフォルダにアップロードされます。この場合、バッファは既にテンポラリに過ぎないので更新したい場合はファイルブラウザから開きなおして下さい。

まとめ

まとめると...
モテる Vimmer に読み書き出来ないファイルなど無かったんだよ!!
ってことです。

追記
モテる度合いには個人差がございます。
Posted at by