vim-noqalign を作った

同僚氏が noqalign と言うツールを作った。


今のお仕事では Python の `__init__.py` にモジュールを import する。
import だけしてるので flake8 対策として `# noqa: F401` をつけるが、これを面倒と感じた同僚氏が自動で入れるようなツールを作った。


お仕事が佳境な状況だったけど、息抜きに Vim plugin から使えるようにした。
GitHub - heavenshell/vim-noqalign: noqalign wrapper for Vim
超便利!

vim-prettier を作った

tl;dr
GitHub - heavenshell/vim-prettier: Prettier-Eslint-Cli for Vim


Prettier と言う JavaScript のフォーマッターがある。
こいつ単体ではお仕事で設定している ESlint の設定とは関係なくフォーマットするが、prettier-eslint と言うのがあり、これを使えば ESLint の設定通りにフォーマットしてくれる。


で、フォーマッターはエディタから設定したくなる。
オフィシャルのドキュメントには以下のように使えばいいよとある。

autocmd FileType javascript set formatprg=prettier\ --stdin
autocmd BufWritePre *.js :normal gggqG

お手軽だけど、同期的に動く。
そして Prettier は速くはない。
当然 Vim から使えば、その間ブロックされる。

おそらく neoformat とやらも同期で動く。
https://github.com/sbdchd/neoformat/blob/master/autoload/neoformat.vim#L82

Golang の Fmt なんかは速いので同期でも問題ないが、 Prettier は同期では辛い。


というわけでいつも通り Vim の job を使って作った。
ただ、いくつか罠があった。

出力先がバッファ

job は出力先にバッファなファイルを選択できる。
この場合はフォーマットした結果をバッファにて書き換えたいから、バッファを選択してみた。

let s:job = job_start(cmd, {
        \ 'callback': {c, m -> s:callback(c, m)},
        \ 'err_cb': {c, m -> s:error_callback(c, m)},
        \ 'in_io': 'buffer',
        \ 'in_name': file
\ })

こうすると、現在開いているバッファ(file で指定しているバッファ)に追記される。
追記じゃなくて、書き換えてほしい。

出力先がバッファ
let s:job = job_start(cmd, {
        \ 'callback': {c, m -> s:callback(c, m)},
        \ 'err_cb': {c, m -> s:error_callback(c, m)},
        \ 'in_io': 'file',
        \ 'in_name': file
\ })

こうすると、現在開いているファイルをバックグラウンドでフォーマットした結果に書き換える。
現在開いているバッファは書き換わる前の状態。
この状態で :e! でファイルを開き直したら、書き換わった状態に変わる。
当然ファイルが書き換わってるので、フォーマットを実行したらファイルが保存される。


前に作った ESLint の --fix モードもこの方式で実装した*1
vim-frontier/eslint.vim at master · heavenshell/vim-frontier · GitHub


Vim-Swift などもこんな感じで行っている。
https://github.com/dictav/vim-swift/blob/master/autoload/swift/fmt.vim#L10


保存と同時に実行するという制約をつければいいが、なんか釈然としない。
せっかく stdin で受け取れるので、フォーマットを実行したら、保存されずにそのままにしてほしい。
他にないかなと探したら、 id:haya14busa さんの GitHub - haya14busa/vim-gofmt: Formats Go source code asynchronously with multiple Go formatters. でおおっと思った実装がされていた。
Add silent for avoid message by heavenshell · Pull Request #1 · haya14busa/vim-gofmt · GitHub *2
tmp なファイルにフォーマット後のコードを吐き出して、ファイルを読み込んで現在のバッファを全て消してバッファを書き換えていた。


prettier は stdout でフォーマット結果を返してくれるので、job の callback で受け取ってリストに追加、 exit_cb で追加されたリストの値で復元とすれば行けた。

*1:あとで書き換えるかも

*2:最新のコミットで改善されてたようだけど

Vim を mocha から実行する際の注意

QuickRun を利用して、 Vim から各種テストフレームワークを実行してる。
GitHub - heavenshell/vim-quickrun-hook-unittest: Quickrun hook for enable to unittest by selecting method.

mocha も実行できるようにしたけど、mocha.opts に自分のプロジェクトのヘルパーファイル名 js ファイルを読み込んでるとエラーになるのがわかった。

--require test/helper.js
--require babel-register
--recursive

こんなのがあると、テストの実行が、package.json とかがあるディレクトリから実行すると問題ないが、テストファイルのあるディレクトリから実行すると、 `test/helper.js` が実行ファイルからの相対パスとなって、そんなファイルないと怒られる。


結局アドホックに mocha.opts の内容を QuickRun 実行時に読み込んで `--require` がある行を mocha の cli オプションに渡すことにした。
そしてファイルが `.js` で終わるファイルは多分ローカルプロジェクトのものだろうという雑な感じで、フルパスに変換してやる。


ということで、テストファイルの特定のテストを実行できるようになった。

SQLALchemy seed を作った

お仕事で Django を使ってて便利だなーと思ったのが fixtures。
初期データを突っ込むのに便利。
Providing initial data for models | Django documentation | Django


yaml で初期データの定義を書いて、コマンド経由で DB にデータを入れられる。
Flask で似たようなことできないだろうかと探したら、Flask-Fixtures があったが、これはテストデータを入れるのを想定している模様。
Add load_fixtures_from_file by RoPP · Pull Request #21 · croach/Flask-Fixtures · GitHub という PR もあるが放置されている。


また Flask-SQLAlchemy に依存しているので、素の SQLALchemy を使ってる場合は使えない。
SQLAlchemy-Fixtures というものがあるが、Factory_boy を使えとある。


もう一つの方法としてはマイグレーションツールの Alembic の中で bulk_insert を使って初期データを入れる。
が、autogenreate を使う場合を想定すると、マイグレーションファイルとは別にしたい。


というわけで SQLAlchemy と PyYaml*1だけに依存しているものを作った。
GitHub - heavenshell/py-sqlalchemy_seed: Simple data seeder using SQLAlchemy.

こんな感じのディレクトリ構成。

.app
  /fixtures/pictures.yaml
  models
  /models.py
  /db.py
  views.py

Seed ファイルを書く。
書き方は Django と同じ(多分)。

- model: app.models.picture.Picture
  id: 1
  fields:
    name: spam.jpg
    title: Spam
    description: spam description
- model: app.models.picture.Picture
  id: 2
  fields:
    name: ham.jpg
    title: Ham
    description: ham description
- model: app.models.picture.Picture
  id: 3
  fields:
    name: beacon.jpg
    title: Beacon
    description: beacon description

こんな感じで、 yaml ファイルを書く。

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker

engine = create_engine('sqlite:///./db.sqlite', convert_unicode=True)
Base = declarative_base()
Base.metadata.bind = engine
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
session = scoped_session(Session)

モデルはこんな感じ。

from sqlalchemy import Column, Integer, String
from .db import Base


class Picture(Base):
    __tablename__ = 'pictures'
    id = Column(Integer, primary_key=True)
    name = Column(String(254), nullable=False)
    title = Column(String(254), nullable=False)
    description = Column(String(254), nullable=False)

    def __init__(self, name, title, description):
        self.name = name
        self.title = title
        self.description = description

呼び出し側はこんな感じ。Flask-Script を使ってみる。

import os
from flask_script import Manager, prompt_bool
from sqlalchemy_seed import load_fixtures, load_fixture_files
from app import create_app
from app.models.db import session


@manager.command
def loaddata(filename=''):
    """Load seed data."""
    if filename == '':
        return

    from app.models.db import session
    path = os.path.join(app.root_path, 'fixtures')

    fixtures = load_fixture_files(path, [filename])
    load_fixtures(session, fixtures)

動かしてみる。

$ python manage.py loaddata -f pictures.yaml
$ sqlite3 db.sqlite
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> select * from pictures;
1|spam.jpg|Spam|spam description
2|ham.jpg|Ham|ham description
3|beacon.jpg|Beacon|beacon description
sqlite>

便利になった。

*1:多分JSON ファイルも行けると思うし、機能は入れてるが未確認

Python の Flake8 と Mypy のプラグイン作った

tl;dr
Flake8 と Mypy のチェックが動く Vim プラグイン作った。


Flake8 のプラグインは何個あるんだって感じだが、GitHub - kevinw/pyflakes-vim: on the fly Python checking in Vim with PyFlakespyflakes-vim はとっくに deprecated だし、Python3 の type-hint な文法は解釈できないし、既存の Flake8 なプラグイン(Syntactic, neomake) は保存をしないとチェックが走らないし、Syntactic に至っては遅いし、khuno.vim は保存しなくてもリアルタイムにチェックしてくれるが、QuickFix 使わず独自のバッファでエラーリスト管理してるし、どれも気に入らないから結局自分で作った。
GitHub - heavenshell/vim-snowflake: An asynchronous Python source code checker for Vim.

特徴

例によって job と channel で動作している。
デフォルトは Flake8 だが、frostedpylama もパーサー書けば動く。
Flake8 は stdin から受け取るオプションがあるので、バッファをそのまま流し込んでるだけ。
そのため保存しなくてもチェックが走る。
InsertLeave や TextChange あたりに autocmd を設定すれば良い。
Python3 な venv で Flake8 を入れたら当然 type-hint なものもエラーではなくて、解釈してくれる。

MyPy

せっかく type-hint が正しく通るようになったから、型チェックもやらしたい。
現状 Mypy で Vim と言ったら @_achiku さんの以下のエントリにあるよう、Syntactic を使うしか無い模様。
https://akirachiku.com/post/2015-11-04-syntastic-mypy/


Mypy にはオプションに stdin はなく、Guido 御大も stdin オプションをつけたく無いと言われてるので、入ることはなさそう。
Feature request: checking files on stdin · Issue #2119 · python/mypy · GitHub
その代わりに `--shadow-file` というドキュメントに書いてない隠しオプションあるから、そっち使ってねとある。
Feature request: checking files on stdin · Issue #2119 · python/mypy · GitHub


`-shadow-file` の説明はソースコード内に書いてある。
mypy/main.py at master · python/mypy · GitHub

cp app.py tmp.py
mypy --incremental --silent-imports --show-column-numbers --shadow-file app.py tmp.py 

と動かすと、チェックされない。

mypy: error: Missing target module, package, files, or command.

正しく動かすには以下のようにする。

mypy --incremental --silent-imports --show-column-numbers --shadow-file app.py tmp.py app.py

チェックする対象のファイルが必要な模様。
app.py を指定してるんだからそれで良さそうなのに、コード読むと必要みたい。


tmp.py には Vim のバッファの内容を読み取って、 `tempname()` でテンポラリにファイルを作成したやつを指定する。


これで type-hint な Python コードも静的チェックができるようになった。
アンドキュメントなやつだし、使えなくなる可能性高いけど便利。


実際に Flake8 と Mypy を同時に job を使いつつ TextChange イベントとかでフックすると重そうだけど、実際のプロダクト書くときに使ってみてドッグフーディングしてみる。


こういうツールを作ってる人にはエディタからの連携が楽になるので是非 stdin をサポートしてほしい。

2016 年振り返り

かなり激しい一年だった。

デスマった。春先に倒れた

見事に 2 月から 4 月までコミットがない。
特に 4 月は体調が最悪だった。
3 月末に案件が終わってホッとしたところで見事に倒れた。
いのちたいせつに。

転職した

働き方を変えようと思ってたところにご縁があって声をかけてもらって、わがまま聞いてもらってフルリモートで働かせてもらってる。
今の所快適に働けている。

GitHub のプライベートリポジトリを使ってるので、草は増えた。
プライベートなコミットはほぼ Vimプラグインになってしまった。
理由があって、仕事で自分が楽したいためにプラグイン探す -> 気に入らない -> 作ってしまえという循環にはまったため。
特に Job, Channel, Timer あたりが出てきて非同期の恩恵に与ってないプラグインが多いので、そこの不満を自分で解消している感じ。

リモートワークしてる

リモートワークは最初はオン・オフの切り替えが難しかったけど、最近は割と出来るようになってきた。
ハイコンテキストな会話をされるとついていけないけど、そんなのは最初から覚悟しているし問題ない。
寂しくないの?と何度か聞かれたが、フルリモートワークしてる先人たちのように一緒に暮らしてる家族がいないので寂しいかなと思ったが、別にそんなこともない。
Slack に行けば会社の人がいるし。これが何か投げても何も返ってこないようなところだと辛そうだけど、幸いにしてそういうこともない。


諸事情があって 1 週間ほど家で働いてなかったけど、ネットさえ繋がっていれば仕事はできる。
そういう風に入る前に環境を整備してきた会社の人たちには感謝しかない。
社内サーバーがないというのは素晴らしい。


リモートワークするにあたって良い椅子(オカムラのバロン)と机(ビクターワークスタジオ)を買ったけど、買ってよかった。
特に椅子はお高いだけあって非常に快適。研究員時代の椅子と同じにしてよかった。
ワークスタジオは奥行きが 50cm にしたけど、60cm でもよかったかも。
もう少し広い部屋に引っ越したいなと思ったりもしないでもない。

フロントエンド

  • TypeScript はじめた
  • FlowType はじめた

転職してフロントエンドの方を触る機会が多くなった。一から自分で調べるよりフロンエンドに詳しい人がいるのでかなり助かっている。
TypeScript は文法的に FlowType より好みだけど、2.0 までは外部ライブラリ使うのに型定義ファイル絶対必須なのは開発の速度が辛い。
2.1 になって緩くなったようだけどまだ追いついてない。

型定義も仕事で必要なので 3 つほど Pull Request した。
1 つは FlowType を使うことになったので、放ったままになってしまってる。なんとかせんと。

Django はじめた

一方でバックエンドは Django を使ってるので、Django はじめた。
はじめてまともに触ったけど、諸々揃っていて楽。
こちらも精鋭が揃ってるので、すごく楽させてもらってる。


Django ならこうやって書くというのをレビューで指摘もらえるのは本当にありがたい。
SQLAlchemy 大好きなので、まだ ORM は慣れてないけど、そのうち慣れると思う。

まとめ

健康に気をつけたい。

FlowType のプラグイン作った

この記事は Vim アドベントカレンダー 2016 の23日目の記事です。
id:yuttie さんの comfortable-motion.vim よさそうなので入れてみたが自分の MacVim な環境では "E118: 関数の引数が多過ぎます: 128_tick" が出たので追いかけようと思います。
…と思ったけど、修正版が上がってたのでしばらく使ってみよう。
慣性スクロール素敵。


TL;DR

  • FlowType のプラグイン作った
  • complete() の微妙な挙動に気づいた
  • パッチ作るために Vim をいじった

はじまり

Facebook の FlowType ようの Vim プラグインは有る。
GitHub - flowtype/vim-flow: A vim plugin for Flow

諸々挙動が気に入らなくて、最初 PR しようかと思ったが、ローカルの npm のモジュールを使う Pull Request が放置されたり、PR の敷居が高そうだし、直すなら全部書き換えくらいな雰囲気なコードなので自分専用で作った。
GitHub - heavenshell/vim-flood: A simple Vim plugin for facebook flow


どうせなら、job と channel 使って非同期で色々やりたかった。
型チェックのエラー表示は QuickFix に非同期で出す、補完も非同期でやる方法を知りたかった。
# ちなみに Flow は割と速いので同期と非同期の差はほとんど無い


今現在 Flow が持つ機能全て実装してある。
TypeScript には GitHub - Quramy/tsuquyomi: A Vim plugin for TypeScript という素晴らしい IDE があるが、これには及ばない。
エディタ機能については Language Service がある TypeScript が勝ってる。

非同期の補完

同期の補完を作るのは omnifunc を使ってやればいい。簡単。
非同期の場合は、どうやったらいいのか最初分からなかったが、miyakogi さんの asyncjedi に答えがあった。

を独自の補完関数にマッピングしてやり、補完候補のリストを作り、complete() に渡せば良い。

complete() の微妙な挙動

一通り機能を実装し終わった後に実際に使っていると、補完候補が 1 件の場合の動作が混乱した。
complete() は補完候補を作るが、completeopt で menu かつ noinsert や noselect を指定している場合の挙動がわかりづらかった。


noselect や noinsert は補完候補が沢山ある場合は、候補のみを表示して、実際には補完せずユーザーに選ばせる。
menu は補完候補が 1 件の場合はそれが挿入される。
また普通の omnifunc の場合補完時はエコーエリアに何が起きているかを表示してくれるが、complete() はしない。


つまり補完候補が 1 件の場合、何も表示されず補完候補が何もないように錯覚する。
# 指摘されて気づいたが completeopt=menuone,noselect とかのように menuone の場合は起きない


なんか分かり辛いなーと思い、この挙動を自分で変えられないかと思った。

Vim のコードを読む。

とりあえず vim のコードを clone して src の下で grep する。
キーワード的に noinsert とか noselect あたりを使うと、edit.c に当たる。
edit.c を読んでいくと、set_completion という関数に当たる。


complete() 時noselect や noinsert が何をやっているかというと、補完時に completeopt にそれらがあれば、KEY_DOWN と KEY_UP のイベントを中で呼び、補完候補の選択位置を変えてる。
vim/edit.c at master · vim/vim · GitHub


そのため completeopt=menu,noselect,noinsert の場合は、補完候補の位置が隠れている状態になる。


ということは補完候補が 1 つなら completeopt=menu と同じ挙動にすれば良いと思う。
本来なら complete() 時もメッセージ出してやればいいのだろうけど、いじる箇所が多くて大変そうなので、簡単な方法から試してみる。


変更を入れる場所がわかったので、あとは補完候補の数を調べれば良い。
compl_length というそれっぽい変数があったので、これかと思い、条件文を加えて、ビルドして Vim を動かしてみたが、うまく動かなかった。


やむをえないので gdb を使う。
ついでに Mac より Linuxデバッグした方がやりやすいので、VMUbuntu を立ち上げ、Vimデバッグオプション付きでビルドする。
出来上がったバイナリを gdb 経由で起動し、set_completion にブレークポイントを張る。


簡単に再現するように以下のような Vim script をでっち上げる。

set completeopt=menu,noselect
inoremap <F5> <C-R>=ListMonths()<CR>

func! ListMonths()
  call complete(col('.'), ['January'])
  return ''
endfunc

inoremap <F6> <C-R>=ListMonths2()<CR>

func! ListMonths2()
  call complete(col('.'), ['January', 'Feb'])
  return ''
endfunc

F5 や F6 を押せば自動的に gdbブレークポイントを張ったところで止まる。
で、compl_length の変数の中身を見たが違った。
どこかで補完候補を持ってるはずと、コードを眺めていたら、引数で list_T *list という引数がある。
この中を gdb で見ると、lv_len というものを持っており、これに補完候補数が格納されている。


というわけでこれを使えば良い。
noselect_patch.diff · GitHub
できた。テストとかはまぁ後でいいやと思い、vim-jp にぶん投げて反応を見る。
補完候補が 1 つの際の noselect,noinsert の挙動 · Issue #984 · vim-jp/issues · GitHub


で、menuone でええやんと教えてもらうが、menu の時の挙動がわかりづらいことを説明して、vim_dev に仕様変更としてどうよ?と投げてもらったが、既存補完系のプラグインの動き壊すと言われた。


まぁそうだよなーもっともだし、completeopt が menu,noselect,noinsert の時のみこの変更が動くようにすればいいのかなーと思ったが、completeopt=menuone,noselect,noinsert なら困らんしとモチベーションが若干落ちたので、放置したままになってしまった。
意欲が湧けばまた取り掛かる。

結論らしきもの

Vim のコードをいじるのは今でもハードルが高いが、コードを変更して、make すればいいだけなので、ここら辺は昨今のフロントエンドの開発や Golang の開発と変わらないなーということで、みんなもっと、パッチ書けばいいと思う。