2014/02/04


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



2013/12/06


この記事はVim Advent Calendar 2013 : ATND 7日目の記事になります。

2013年もあと少しです。今年も Vim に関する色んな話題が登場しました。
そして数多くの Vim plugin が今年も登場しました。
  • ステータスバーをスタイリッシュにするプラグイン
  • Markdownをプレビューするプラグイン
  • 正規表現をプレビューするプラグイン
  • ブラウザと通信するプラグイン
  • 補完プラグイン
  • 貴様!?まさか!
  • そのまさかだ!フハハハハハハ...
どれも凄い物ばかりでした。そして Vim 界においては日本人の活動が特に素晴らしかったと思っています。 k_takata さんの大活躍で多くのパッチが vim-jp より提出され取り込まれました。
何名かの方には Vim 7.4 のリリース後も動作検証に協力頂きましたし、Vim advent calendar 2012 は結局1年を通して記事をポストし続けてしまいましたし、vimrc読書会も毎週続けられ、Vim に関するイベントも多く開催されました。
個人的には9月に KoRoN さん、h_east さん、ytaniike さんとお好み焼きを食べに行った事も良い思い出になりました。ありがとうございました。

来年も Vim にとって良い年である事を切に願います。
さて、今年作られた Vim plugin、どれも良い物でしたが僕がいつも思う事はこんな事です。

Vim プラグインとは言え、作るからには人々の暮らしを豊かにしなければならないッ!
僕は Windows を良く使います。僕の Vim script 力で Windows ユーザを豊かにしたい、くじけそうになっている人を Vim で救いたい。モテたいと願っている青年達を Vim でモテモテにしたい。そう願って今年も新しいプラグインを作りました。
mattn/startmenu-vim - GitHub
https://github.com/mattn/startmenu-vim
StartMenu

CtrlPUnite で Windows のスタートメニューが扱えます。
これでいちいちマウスに手を伸ばしてスタートメニューをクリックし、ポチポチとツリーを展開したり検索語を入力していく必要もありません。

あえて日本語ロケールの名称で一覧していませんので、IME を使わず英名でマッチします(ショートカット名が日本語の場合は頑張って下さい)。これで Vim から Excel 等を簡単に起動出来る様になりました。
会社で Vim から Excel 起動したら、きっと同僚の眼差しも「センパイッ!すごい!」となるでしょう。
もっと豊かに、もっと豊かに...

そうだ...そうなんだ。僕のプラグインで便利になったのはプログラムの起動方法でしかないッ!Windows ユーザはそのランチャーから起動した Excel で設計書を閲覧するんだ!彼らの生活を豊かにする為には、Vim で Excel を開けなければならないんだァッ!!これはもう、Vim で Excel を見れる様にするしかない!!1
そう思って、もう一つプラグインを作りました。
mattn/excelview-vim - GitHub
https://github.com/mattn/excelview-vim
てっとりばやく言うと、Vim で Excel を閲覧出来ます。 :ExcelView Book1.xls で表示出来ます。シート番号を指定して :ExcelView Book1.xls 2 という開き方も出来ます(この辺は、今後もしかしたら変更するかもしれません)。
excelview1
こんな Excel ファイルであれば
excelview2
こう表示されます。
xlsx 形式(Excel 2007形式)のファイルしか読めないです。今後、もしかしたら CSV には対応するかもしれません。実行には webapi-vim が必要です。
ちなみに Pure Vim script の XML パーサでこんな事やってます。
let doc = webapi#xml#parse(xml)
let rows = doc.childNode("sheetData").childNodes("row")
let cells = map(range(1256)'map(range(1,256), "''''")')
let aa = char2nr('A')
for row in rows
  for col in row.childNodes("c")
    let r = col.attr["r"]
    let nv = col.childNode("v")
    let v = empty(nv) ? "" : nv.value()
    if has_key(col.attr, "t") && col.attr["t"== "s"
      let v = ss[v]
    endif
    let x = char2nr(r[0]) - aa
    let y = matchstr(r, '\d\+')
    let cells[y][x+1= v
  endfor
endfor
今の所、制限事項として 256x256 のデータしか表示出来ません。それ以上のデータを読み込むとエラーが発生します(改良の予定です)。

これでとても豊かになりました。ssh でサーバにログインしている開発者が Excel で仕様書を送りつけられても椅子を投げる事も無くなると信じています。

良い年末をお過ごし下さい。
実践Vim 思考のスピードで編集しよう! 実践Vim 思考のスピードで編集しよう!
Drew Neil, 新丈径
アスキー・メディアワークス 単行本(ソフトカバー) / ¥4,880 (2013年08月29日)
 
発送可能時間:

Vimテクニックバイブル ~作業効率をカイゼンする150の技 Vimテクニックバイブル ~作業効率をカイゼンする150の技
Vimサポーターズ
技術評論社 単行本(ソフトカバー) / ¥363 (2011年09月23日)
 
発送可能時間:

Posted at by



2013/09/17


最近の Vim の使われ方は明らかに数年前とは違う物で、新しい使われ方がどんどんと増えてきています。知識を共有する事で自分なりの Vim を見つける事が出来ます。
生産性を向上させるVimのTips」と題して普段の生活でどの様に Vim を活用し、どの様に Vim の情報を収集していくかを説明してみました。
その他、コラムも含めて Vim を普段から知っている人であればニタニタ出来る記事になったと思っています。

ぜひお手に取ってご覧ください。

Software Design (ソフトウェア デザイン) 2013年 10月号 [雑誌] Software Design (ソフトウェア デザイン) 2013年 10月号 [雑誌]

雑誌 / ¥960 (1970年01月01日)
 
発送可能時間:

Posted at by