読者です 読者をやめる 読者になる 読者になる

comfortable-motion.vim の紹介

この記事は Vim Advent Calendar 2016 の22日目の記事です。

最近のVim/Neovimで導入されたタイマーAPIを活用して、物理シミュレーションによるスムーススクロールプラグインを作ったよ!という内容です。

紹介するプラグイン: https://github.com/yuttie/comfortable-motion.vim

更新情報は末尾に記載しています。

はじめに

Vimの効率的な編集操作に惚れてしまい、長くVimを使っています。といっても最近は Emacs 上で Vim と同等の操作(テキストオブジェクトなども含む)を実現する Evil というプラグイン、そしてそれをベースにした Spacemacs という Emacsディストリビューションも使っていたりして、交互に VimEmacs の世界を行き来するのが数年続いています。(正確には今は Neovim を使っています。)

Emacs を使っていて個人的に外せなかったプラグインは、@kiwanami さんの emacs-inertial-scroll です。これは inertial 「慣性の」 が示す通り、画面のスクロールに慣性を持たせることで、スクロールをスムースにするものです。スクロールをスムースにする方法は他にもありますが、このプラグインは、

  • 物理法則ベースのアニメーション
  • 行単位でしかスクロールできない環境でのスムース化

を実現している点で感銘を受けました。ブラウザみたいなピクセル単位でスクロール可能な環境では、拡張機能によって物理ベースのスクロールの実装が既に登場していましたが、EmacsVimのようなエディタにおいては見たことがありませんでした。

スムースなスクロール

コンピュータ上のUIの悪い所は、全ての動作が必ずしも連続的ではない点だと思います。実際に古くからUIにアニメーションが導入されたり、最近の Microsoft OfficeGoogle の Material Design で様々なものに滑らかな動作を与えようとしている取り組みは、やはり彼らもこの点について問題意識をもっていて改善をしようとしているからだと思います。

エディタのスクロールにおいても連続性が重要となる場面があります。編集している内容に依存はしますが、似たような構造の記述が連続して存在するような場合(XMLJSONなどのデータだと特にそうですね)や、自分で書いたのではないソースなどの場合などです。画面が切り替わった後、自分がそれまで着目していた箇所がどこに移動したのか?これを探す手間をスムーススクロールによって無くすことができるのは、個人的には大きな利点です。

最近の Vim や NeoVim にタイマー系の機能が実装されたことで、ついにスムーススクロールが実現できるようになりました。タイマーによって非同期に処理を走らせることができるので、UIを固めることなくアニメーションを実装することができます。タイマーを用いて非同期処理を実現する方法は JavaScript でもおなじみですね。

そこで今回、初自作Vim/Neovimプラグインとしてcomfortable-motion.vimを作成しました。実は前にFirefoxVimperatorプラグインとして物理ベーススムーススクロールを実装したことがありまして、この時のコードを移植することで実装しました。名前には、滑らか、かつ自分好みの動きを実現したいという思いを込めてあります。

実際の動作

[2016-12-31加筆] Timer APIを利用するため、Vim 7.4.1578 以上、もしくは Neovim 0.1.5 以上が必要です。ただし動作確認はVim 8.0 と Neovim 0.1.7 上で行っています。

インストール方法などは README.mdを見てもらうとして、実際の動きを見てもらいましょう。

  • C-u / C-d によるスクロール

f:id:yuttie:20161221204939g:plain

  • C-f / C-b によるスクロール

f:id:yuttie:20161221204943g:plain

動画中では行っていませんが、スクロール中に別の操作を行うことも可能で、例えばC-fを連続で入力すれば速度を上げることができますし、C-fを入力した直後にC-bを入力すればスクロールがいくらかキャンセルされます。

ちなみにこのGIF動画は byzanzで撮っています。画面上の差分だけを記録してコンパクトなGIFファイルを生成してくれるのでお薦めです。あとカラースキームは自作のを使用しています。

原理

基本的にはキーを押すことで速度(力積)を与え、2種類の抵抗力により速度を減衰させるというのをシミュレートしています。抵抗力としては空気抵抗と摩擦抵抗の2種類の力を採用しています。前者ではその時の速度の2乗に比例する抵抗力が生じるのに対し、後者では常に一定の抵抗力が生じるという違いがあります。

抵抗力のパラメータをいじることで、どちらをどのくらい強くするかを調整でき、その比率によって多様な減衰曲線を表現できるのが2つの抵抗力を採用するメリットです。例えば、空気抵抗をより重く考慮するように設定すれば、例えばC-fを連続で入力して速度を上げた場合にすぐ減衰するようになります。また、一時的に下の2つを0にすればオートスクロールも実現できます。

現状で提供している設定用のグローバル変数は3つあり、その内2つはこれらのパラメータに対応しています。

  • g:comfortable_motion_interval: シミュレーションの間隔 [default: 1000.0 / 60]
  • g:comfortable_motion_friction: 空気抵抗パラメータ [default: 80.0]
  • g:comfortable_motion_air_drag: 摩擦抵抗パラメータ [default: 2.0]

1つ目は基本的にはデフォルトから変更する必要はないと思います。残りの2つをご自分の好みに合わせて調整して下さい。デフォルトの設定は私の好みに合わせつつ、C-u/C-dでおよそ25行分スクロールするような設定に調整してあります。README.mdには、どちらか一方だけを使う極端な設定例も載せてありますので、まず両方試してみてその間にある好みの設定を見つけてもらうのが良いかと思います。

さいごに

今回はタイマーAPIの機能を活用することで、これまで実現できなかった機能を実現するという例を紹介しました。Atomのようなモダンなエディタや、Vimの大規模な拡張を行うNeovimの登場により、Vim界隈の活気がみなぎっているのはとても素晴らしいと感じています。またEmacsでは最近、マルチスレッドへの対応が議論されているようです。

基本的な編集機能を追求するVimEmacsといったエディタ達が、来年も更なる飛躍を遂げることを心より願っていますし、また自分も何かしら貢献できれば思います。

ありがとうございました!


更新情報

Update on 2016-12-22 20:23 GitHub上で以下のようなIssue #1を上げて頂いています。 現在問題を調査中です。 github.com

Update on 2016-12-22 21:05 Issue #1を修正完了しました。これでVim 8とNeovimの両方で動くはずです。 うーん、意外と細かい所で互換性が無いんですね。

Update on 2016-12-31 20:39 GitHub上で100個の★を頂きました! 当初これだけの反響があるとは思ってもいませんでした。慣性スクロールを愛するVimmerの方々のお役に立つ事ができ嬉しい限りです。 こういう小さなプラグインであっても、公開することの意義を実感しました。

Clangのコード補完機能とシステムインクルードファイルの問題

C++ Clang Emacs Vim コード補完

Clangのコード補完機能

Clangを用いるとC/C++のコード補完を行うことができます。

例として次のようなC++コードを考えてみます。

#include <vector>

int main(int argc, char* argv[]) {
    std::vector<int> v;
    v.
    return 0;
}

ここでv.の直後における適切な補完候補(つまりstd::vectorのメンバ)の一覧をClangに出力させてみましょう。 test.cppにこのコードが含まれているとすると、v.の直後の位置はコードの5行7列目であるので、次のコマンドラインにより出力させることができます。

% clang -fsyntax-only -Xclang -code-completion-at=test.cpp:5:7 test.cpp
COMPLETION: assign : [#void#]assign(<#size_type __n#>, <#const value_type &__val#>)
COMPLETION: assign : [#void#]assign(<#_InputIterator __first#>, <#_InputIterator __last#>)
COMPLETION: at : [#reference#]at(<#size_type __n#>)
COMPLETION: at : [#const_reference#]at(<#size_type __n#>)[# const#]
COMPLETION: back : [#reference#]back()
COMPLETION: back : [#const_reference#]back()[# const#]
COMPLETION: begin : [#iterator#]begin()
COMPLETION: begin : [#const_iterator#]begin()[# const#]
COMPLETION: capacity : [#size_type#]capacity()[# const#]
COMPLETION: clear : [#void#]clear()
COMPLETION: data : [#pointer#]data()
COMPLETION: data : [#const_pointer#]data()[# const#]
COMPLETION: empty : [#bool#]empty()[# const#]
COMPLETION: end : [#iterator#]end()
COMPLETION: end : [#const_iterator#]end()[# const#]
COMPLETION: erase : [#iterator#]erase(<#iterator __position#>)
COMPLETION: erase : [#iterator#]erase(<#iterator __first#>, <#iterator __last#>)
COMPLETION: front : [#reference#]front()
COMPLETION: front : [#const_reference#]front()[# const#]
COMPLETION: get_allocator : [#allocator_type#]get_allocator()[# const#]
COMPLETION: insert : [#iterator#]insert(<#iterator __position#>, <#const value_type &__x#>)
COMPLETION: insert : [#void#]insert(<#iterator __position#>, <#size_type __n#>, <#const value_type &__x#>)
COMPLETION: insert : [#void#]insert(<#iterator __position#>, <#_InputIterator __first#>, <#_InputIterator __last#>)
COMPLETION: max_size : [#size_type#]max_size()[# const#]
COMPLETION: operator= : [#std::vector<int, std::allocator<int> > &#]operator=(<#const std::vector<int, std::allocator<int> > &__x#>)
COMPLETION: operator= (Hidden) : [#std::_Vector_base<int, std::allocator<int> > &#]std::_Vector_base<int, allocator<int> >::operator=(<#const std::_Vector_base<int, std::allocator<int> > &#>)
COMPLETION: operator[] : [#reference#]operator[](<#size_type __n#>)
COMPLETION: operator[] : [#const_reference#]operator[](<#size_type __n#>)[# const#]
COMPLETION: pop_back : [#void#]pop_back()
COMPLETION: push_back : [#void#]push_back(<#const value_type &__x#>)
COMPLETION: rbegin : [#reverse_iterator#]rbegin()
COMPLETION: rbegin : [#const_reverse_iterator#]rbegin()[# const#]
COMPLETION: rend : [#reverse_iterator#]rend()
COMPLETION: rend : [#const_reverse_iterator#]rend()[# const#]
COMPLETION: reserve : [#void#]reserve(<#size_type __n#>)
COMPLETION: resize : [#void#]resize(<#size_type __new_size#>{#, <#value_type __x#>#})
COMPLETION: size : [#size_type#]size()[# const#]
COMPLETION: swap : [#void#]swap(<#std::vector<int, std::allocator<int> > &__x#>)
COMPLETION: vector : vector::
COMPLETION: vector : [#void#]vector(<#_InputIterator __first#>, <#_InputIterator __last#>{#, <#const allocator_type &__a#>#})
COMPLETION: ~_Vector_base : [#void#][#_Vector_base<int, allocator<int> >::#]~_Vector_base()
COMPLETION: ~vector : [#void#]~vector()

それらしい候補が出力されていますね。

システムインクルードファイル問題

Clangのコマンドclangには、指定するフラグに応じて「ドライバ」を呼び出す場合と、「フロントエンド」呼び出す場合とがあります。 単純にclangとして呼び出すと、GCCと互換性のある「ドライバ」を呼び出します。 また、clang -cc1として呼び出すと「コンパイラフロントエンド」を直接呼び出します。

ドライバはClangのユーザー用のインターフェイスで、内部でユーザーに代わりフロントエンドを呼び出してくれます。ドライバはフロントエンドの呼び出しに際し、ユーザーのシステム向けに適切なフラグを付与して呼び出します。僕の調べた範囲では、例えばシステムの標準インクルードファイルのパス指定などの設定が含まれているようです。 またドライバに対し、特定のフラグをフロントエンドに渡すように指定することもできます。 先程の例では-Xclang -code-completion-at=test.cpp:5:7とドライバに指定することで、内部でclang -cc1-code-completion-at=test.cpp:5:7というフラグが渡されています。

このように、コード補完機能を利用するのに必要な-code-completion-at=...というフラグは、フロントエンド固有のものであり、ドライバのフラグではありません。従って、コード補完機能を利用するだけであればドライバを利用する必要はなく、フロントエンドを直接呼び出せば良いことになります。実際にClangのリポジトリに入っているEmacs用の補完プラグインclang-completion-mode.elでは、フロントエンドを直接呼び出しています(148行目)

しかし、この方法ではドライバがやってくれるインクルードパスなどの自動設定の恩恵にあずかれません。 試しに先の補完候補を出力させる例を、フロントエンドを直接叩いてやってみようとすると

% clang -cc1 -fsyntax-only -code-completion-at=test.cpp:5:7 test.cpp
test.cpp:1:10: fatal error: 'vector' file not found
#include <vector>
         ^
1 error generated.

となり、システムの標準インクルードファイルが見つからないと言われてしまいます。

この問題は実際にEmacsの自動補完プラグインなどでも報告されています。 例えば代表的なauto-complete-clangでも、同様の問題が起きています。解決策も示されていますが、場当たり的な対処でしかありません。 恐らく最初の実行例で示したように、clangドライバに頼るのが良い解決策だと思います。

[追記:2014-02-11 17:00]auto-complete-clangPull Requestを出しました。

Evil で outline-minor-mode を使った folding

標準のキーバインディングには hs-minor-mode を使った折り畳み機能が割り当てられているけど、これを outline-minor-modeを使ってもう少しVimのそれに近くなるようにしてみた。

とりあえず、簡単にできるレベルでVimに近づけてみただけなので、挙動の差異は沢山残っている。
以下コード。

(define-key evil-normal-state-map "zo" #'evil-open-fold)
(define-key evil-normal-state-map "zO" #'evil-open-folds-at-point)
(define-key evil-normal-state-map "zc" #'evil-close-fold)
(define-key evil-normal-state-map "zC" #'evil-close-folds-at-point)
(define-key evil-normal-state-map "za" nil)
(define-key evil-normal-state-map "zA" nil)
(define-key evil-normal-state-map "zm" #'evil-fold-more)
(define-key evil-normal-state-map "zM" #'evil-close-all-folds)
(define-key evil-normal-state-map "zr" #'evil-fold-less)
(define-key evil-normal-state-map "zR" #'evil-open-all-folds)

(evil-define-command evil-open-fold ()
  "Open one fold under the cursor."
  (outline-minor-mode)
  (show-children))
(evil-define-command evil-close-fold ()
  "Close one fold under the cursor."
  (outline-minor-mode)
  (hide-subtree))
(evil-define-command evil-open-folds-at-point ()
  "Open all folds under the cursor recursively."
  (outline-minor-mode)
  (show-subtree))
(evil-define-command evil-close-folds-at-point ()
  "Close all folds under the cursor recursively."
  (outline-minor-mode)
  (hide-subtree))
(evil-define-command evil-close-all-folds ()
  "Close all folds."
  (outline-minor-mode)
  (hide-sublevels 1))
(evil-define-command evil-open-all-folds ()
  "Open all folds."
  (outline-minor-mode)
  (show-all))
(evil-define-command evil-fold-more ()
  "Fold more."
  (outline-minor-mode)
  (when (> evil-fold-level 0)
    (decf evil-fold-level)
    (hide-sublevels (+ evil-fold-level 1))))
(evil-define-command evil-fold-less ()
  "Reduce folding."
  (outline-minor-mode)
  (incf evil-fold-level)
  (hide-sublevels (+ evil-fold-level 1)))

Emacsのフォント設定 for "emacs --daemon"

Emacsdaemonとして起動すると、指定したフォントセット(fontset)が利用されない(というか作られさえしない)問題があって困っていた。

フォントセットが作られないことから、どうやらタイミングの問題なんじゃなかろうか?と思っていたら id:tarao さんの dotfiles リポジトリの中では、既にこの問題に対処した設定がなされていた。
https://github.com/tarao/dotfiles/blob/master/.emacs.d/init/window-system.el

When Emacs is started as a GUI application, just running this
function initializes the configurations.


When Emacs is started as a daemon, this function should be called
just after the first frame is created by a client. For this,
this function is added to `after-make-frame-functions' and
removed from them after the first call."

合わせて考えると、もしかしたら、初フレーム作成の時に fontset の作成に必要な初期化が行なわれている、のかもしれない。

FirefoxのウィンドウUIをタブで開く

FirefoxのUI

FirefoxのいくつかのUI(例えば「設定」など)は通常別ウィンドウ内に表示されます。
一方で、Firefoxの「アドオン」やGoogle Chromeの「設定」や「履歴」などは「タブ内」に表示されます。

XUL: UI記述言語

FirefoxのUIはXULという言語で記述された一種の「ページ(ドキュメント)」であるので、
実際にはそれらのURIが分かれば通常のWebページのように「開く」ことができます。
例えば、「履歴とブックマーク」や「設定」のURIは以下のように表現されます*1

chrome://browser/content/places/places.xul
chrome://browser/content/preferences/preferences.xul

*1:Firefox 23の場合

Haskell: 述語の連結

2つの述語p, qに対して「p(x)またはq(x)である」という述語を作る方法で、モノイドを使う方法。

predOr p q = getAny . (Any . p <> Any . q)

「p(x)かつq(x)である」ならこう、

predAnd p q = getAny . (All . p <> All . q)

ここでは (a -> Any) とか (a -> All) をモノイドと見て連結 (<>) を使っている。

awesome WM でウィンドウをスクリーンの端に配置する

最近awesomeというタイリング型のウィンドウマネージャを使いはじめました。
Lua言語で自由にカスタマイズできるので、Compizなどにもある「ウィンドウを画面の中央または端に配置」する機能を書いてみました。

modキーとテンキーの組合せで、スクリーン内のテンキーと対応する位置にウィンドウを配置します。
1だったら左下、8だったら上、5だったら中央という感じです。