バイナリエディタ bed のコマンドラインの機能を強化しました

先日、自分の作っているバイナリエディタbedhomebrew/coreに入ったことをご報告しました。

その後、コードを眺めていると色々と直したいところが出てきたり、欲しい機能の実装イメージが沸いたりして、また活発に開発するようになりました。 今でもそれなりに使われていることを意識すると、急にメンテする気力が湧いてくるんですよね。不思議です。 個人OSSの継続にはモチベーションが大事です。フィードバックは大歓迎です。

最近、コマンドライン周りの機能強化をやっています。

  • コマンドの実行履歴を上下キー (と<C-n>, <C-p>) で辿れるようにしました。実装としては、実行したコマンドをスライスに追加していく (のと重複を削除する) だけです。少し面倒なのが、コマンド実行 (:) と検索 (/, ?) で別の履歴として保存しないといけないことです。
  • コマンドライン環境変数の補完に対応しました。例えば、$GOPATHのように環境変数ディレクトリを指しているとき、 :e $GOP<TAB>:e $GOPATH/ まで補完して、さらにその中のファイルを候補として表示してくれます。
  • 新しいコマンドとして :cd:pwd を実装しました。:cdは作業中のディレクトリを変更するコマンドで、:pwdはそれを表示するコマンドです。:cd - で前にいた場所に戻るといった細かい (けど使う人には便利な) 機能も実装しています。

全く別の文脈 (とあるVimプラグイン) で作業ディレクトリのことを考えていて、bedでもVimみたいにディレクトリを移動できればファイルを開くのが楽になるなと思い:cdの実装を考え始めたのがきっかけでした。 初めはos.Chdirを呼ぶだけだと思っていたのですが、これまでコマンド実行中にディレクトリが移動することがなかったので、既存の機能に影響して苦労しました。 例えば :w test で保存したウィンドウは、その後 :w で同じファイルを上書きします。 しかし、このtestが作業中のディレクトリからの相対パスになっていたので、:cdを作ったことで挙動が変わってしまいました。 各ウィンドウで絶対パスを管理することで、この問題は解決しました。

コードを読み返していると、当時勢いで書いた部分が多く、色々な箇所をリファクタリングしたくなってきました。 今回は、コマンドラインのパーサーを大きく書き直しました。 昔は[]runeを引き回して次のオフセットを返すようなパーサーをよく書いていたのですが、Goのプラクティス的には文字列を渡してパースしたものと残りの文字列を返す方が良いということがわかってきました。 strings.CutPrefixはとても便利ですね。 他にもやたら状態遷移するコードを好んで書いていたのですが、よほど複雑なものでない限りはコードが読みにくくなるだけなのでやめました。 ここ数年でもだいぶコーディングの好みが変わってきているのを感じます。

テストもだいぶ書き直して、読みやすく安定したものになりました。 bedのテストを見返すと、UIが送ったイベントをウィンドウが処理するのを待つためにsleepするみたいなテストが存在していました。 例えば、UIが:w:qのイベントを送った時に、ファイル保存が完了するよりも前に:qを処理してしまうと、保存がうまくいきません。 でもテストの中でsleepするのは良くないですよね。何を待っているのかわからなくなりますし、その時間で処理が完了する保証もなく、不安定になってしまいます。 UIが送ったイベントに対しては再描画のリクエストがUIに返ってくるので、そのイベントの受信を待つよう修正することでsleepを撲滅することができました。 テストで何かを待ちたくなったら、sleepするより良い待ち方はないのか、本当にsleepする時間内に完了する保証があるのか考える必要があります。

Vimの操作に慣れていて、バイナリファイルを軽く確認したい、軽く編集もしたいという人に、bedはとてもオススメできるツールです。 ぜひ使ってみてください。