Visual Studio .NET をEmacsキーバインドにする方法:
マクロを使ったマーク・リージョン操作の実現

2003年4月28日初稿
2008年8月28日VS2008での状況について追記

背景

Visual Studio .NET(以下VS.NET)のキー操作をEmacsに近づけようとするときに問題になるのがC-SPCに代表されるマーク・リージョン操作です。VS.NETのキーカスタマイズは2ストロークのキー割り当てに対応していますし、ほとんどの機能がキーに割り当て可能になっています。そのためたいていの操作は簡単にEmacs風にカスタマイズできます。しかしEmacsのマーク・リージョン操作はVS.NETに該当するコマンドがないため単純なキーカスタマイズだけでは実現できません。以前のVisual StudioにはC-SPCも含めてもろもろをEmacs風にできるepsilon互換設定があったのですが、VS.NETからは見あたらなくなってしまいました。

(2008-08-28追記)Visual Studio 2005やVisual Studio 2008になって、キーボードスキームとしてEmacsが選べるようになりました。VS2005は色々と不具合があったのですが、VS2008ではだいぶまともになり、この記事のような荒技は不要になったと思います。XKeymacs等と併用すれば、ダイアログ等でもある程度Emacsキーバインドで操作できます。それでも出力ウィンドウでPageUpやPageDownが出来ない等の問題が若干残ります。

概要

この記事ではEmacsのマーク・リージョン操作を実現するVS.NET用のマクロを紹介します。このマクロによってできることを次に挙げます。

また、このマクロによってできないことを次に挙げます。

これらの機能を提供するマクロやVS.NET既存のコマンドを好みのキーに割り当てることで、Emacs風のキー操作が実現可能です。

記事の最後ではこのマクロの動作原理やVS.NET用マクロの簡単な作り方について紹介します。

導入方法

マクロのIDEへの登録

マクロのソースを以下に示します。

新しくマクロプロジェクトを作ってもいいですし、MyMacrosあたりにぶち込んでもかまいません。ソースの内容をIDE内に登録して呼び出せるようにします。

新しくマクロプロジェクトを作るには、VS.NETを起動し、メニューの「ツール(T)」→「マクロ(M)」→「新しいマクロプロジェクト(W)」で適当な名前のマクロプロジェクトを作成してください。私の場合 EmacsKeyBinding にしました。

マクロプロジェクトの中にソースの内容を入れるには、マクロエクスプローラからプロジェクトを右クリックして、「新しいモジュール(D)」を選択。ファイル名を EmacsKeyBinding としてください。できたファイルの中にソースの内容を上書きしてください。

キーカスタマイズ

次に「ツール(T)」→「オプション(O)」の「環境」→「キーボード」でマクロをキーに割り当てます。

本マクロで定義しているプロシージャとEmacs風キー操作との対応を下表に示します。

プロシージャキー
EmacsKeyBinding.SetMarkCommandC-SPC または C-@
EmacsKeyBinding.KeyboardQuitC-g
EmacsKeyBinding.ForwardCharC-f ※注
EmacsKeyBinding.BackwardCharC-b ※注
EmacsKeyBinding.NextLineC-n ※注
EmacsKeyBinding.PreviousLineC-p ※注
EmacsKeyBinding.KillRingSaveM-w
EmacsKeyBinding.KillRegionC-w
EmacsKeyBinding.KillRectangleC-x r k といきたいけど無理なので適当に
EmacsKeyBinding.ExchangePointAndMarkC-x C-x

※注:カーソル移動系のプロシージャ(ForwardChar、BackwardChar、NextLine、PreviousLine)は暫定マークモードを使用しない場合は割り当てなくても構いません。割り当てるとカーソル移動のたびにマクロが呼び出されるため、エディタの動作がやや遅くなってしまいます。ただし、割り当てない場合はカーソル移動時の選択範囲表示ができなくなります。どちらにせよリージョン操作には影響ありません。

よりEmacs風にするため、既存のコマンドに対するキー割り当ても変更しましょう。次に私が行っているキー割り当てを示します。

コマンドキー
編集.貼り付けC-y
編集.1画面分下へ移動C-v
編集.1画面分上へ移動M-v
編集.ドキュメントの開始位置に移動M-<
編集.ドキュメントの終了位置に移動M->
編集.行頭に移動C-a
編集.行末に移動C-e
編集.次の単語に移動M-f
編集.前の単語に移動M-b
編集.インクリメンタル検索C-s
編集.前を検索C-r
編集.右に1文字移動C-f
編集.左に1文字移動C-b
編集.1行下へC-n
編集.1行上へC-p
編集.1語削除C-h
編集.EOLまで削除C-k
編集.削除C-d
編集.単語の最後まで削除M-d
編集.元に戻すC-/
編集.行が中央になるまでスクロールC-l
編集.行に改行を挿入C-m
表示.次のタスクC-x C-`
表示.コマンドウィンドウM-x
ファイル.ファイルを開くC-x C-f
ファイル.閉じるC-x k
ファイル.選択されたファイルを上書き保存C-x C-s
ウィンドウ.ウィンドウC-x C-b

一部のキーは既に登録されている2ストロークキーと衝突します(C-rやC-k、C-m等)。あらかじめ衝突するキー割り当ては削除してください。設定ダイアログが使いにくくて大変ですが。

暫定マークモードの設定

最後に、暫定マークモード(リージョンの強調表示)を行いたい場合はソース中のtransientMarkMode定数の値をTrueに変更してください。暫定マークモードは次のような動作を行います。

XKeymacsとの併用(任意)

このマクロは各種ダイアログやマクロIDEには効果ありません。我慢できない場合は、設定が少々複雑になりますがXKeymacsのようなキーカスタマイズツールと併用するとある程度改善されます。

XKeymacsの設定では、「Microsoft Visual Studio .NET (devenv.exe)」に対するプロパティを変更します。設定のポイントを以下に挙げます。

動作原理

各要素の解説

まずはEmacsKeyBindingモジュール内の各要素について解説します。名前はできるだけEmacsの同等の要素に対応させています。

クラス一覧

MarkRingClass
マークリングを管理するクラスです。16個のマークを保持できます。PushFrontプロシージャで追加、GetAt関数で任意のポイントを取得、SetAt関数で任意のポイントを書き換え、Sizeプロパティでリング内のマーク数を取得、Frontプロパティでリング先頭の取得と書き換えができます。

変数一覧

markRing(MarkRingClass型)
実際のマークリングオブジェクトです。Emacsのmark-ring変数に相当します。本来ならドキュメントごとに持たなければなりませんが、手抜きです。ごめんなさい。
markActive(Boolean型)
暫定マークモードの時、マークがアクティブかどうかを保持します。Emacsのmark-active変数に相当します。

定数一覧

transientMarkMode(Boolean型)
暫定マークモードを使用するかどうかを示す定数です。

プロシージャ一覧

SetMarkCommand
現在の位置をマークリングに保存します。暫定マークモードの場合マークを活性化します。
KeyboardQuit
暫定マークモードの場合、マークを不活性にします。
ForwardChar, BackwardChar, NextLine, PreviousLine
カーソルを移動します。暫定マークモードの場合、選択範囲も一緒に拡張します。もし、現在の選択範囲が記録しているリージョンと異なる場合、選択範囲を修正する処理も行います。暫定マークモードではない場合は基本的にVS.NET既存の移動コマンドと同じことしかしません。
KillRingSave
リージョン(最後にSetMarkCommandされた位置と現在の位置の間)を選択して、コピーコマンドを実行します。暫定マークモードの場合、マークが不活性なら何もしません。このコマンドのあと、マークは必ず不活性になります。
KillRegion
リージョン(最後にSetMarkCommandされた位置と現在の位置の間)を選択して、切り取りコマンドを実行します。暫定マークモードの場合、マークが不活性なら何もしません。このコマンドのあと、マークは必ず不活性になります。
KillRectangle
矩形(最後にSetMarkCommandされた位置と現在の位置との矩形)を選択して、切り取りコマンドを実行します。暫定マークモードの場合、マークが不活性なら何もしません。このコマンドのあと、マークは必ず不活性になります。
ExchangePointAndMark
最後にSetMarkCommandされた位置と現在の位置を交換します。暫定マークモードの場合はマークを再活性化させ、選択範囲を復元する効果もあります。
ReselectRegion
内部的に利用されるプロシージャです。最後のマークと現在位置の間を選択します。
ReselectRectangle
内部的に利用されるプロシージャです。最後のマークと現在位置との矩形を選択します。
RestoreActiveRegion
内部的に利用されるプロシージャです。マークが活性状態の時に、実際の選択範囲と内部的に持っているリージョンが異なる場合に、選択範囲を再設定します。

どのように動いているか

ほとんどの機能はDTE.ActiveDocument.Selectionを操作することで実現できます。(参考:TextSelection オブジェクトのプロパティ、メソッド、およびイベント)

例えばSetMarkCommandで現在の位置を取得するにはDTE.ActiveDocument.Selection.ActivePointプロパティで取得できます。ActivePointはVirtualPointを返すので、画面の影響を受けないEditPointにしてからマークリングに格納しています。

これができれば、最後に記録したマークと現在位置の間にリージョンが形成でき、KillRingSaveやKillRegionが呼び出されたときにリージョンにあわせて選択範囲を再設定して、コピーやカット処理が可能になります。KillRingSaveやKillRegionは呼び出されたときに毎回自分で選択範囲を再設定するので、その前に選択範囲がリージョンと同じになっている必要はありません。

大事なのは選択範囲の維持(つまり暫定マークモード)はおまけであるということです。普通、Emacsのリージョン指定はWindowsアプリでは選択範囲の指定に置き換えられます。しかし数多くある移動コマンドすべてに対応して、常に選択範囲を維持するのはなかなか困難を伴います。検索コマンドやマウスのクリックなど多種多様な方法でカーソルは移動します。それらに対して、C-SPCが押された後だったら選択範囲の拡張をして、そうでなければ普通の移動をするというような処理を挟むのは難しいでしょう。外部のキーカスタマイズソフトを使えばシフトキーを押し下げてエミュレートできます。しかし、思わぬところでキー操作がおかしくなるトラブルが発生する場合があります。せっかくマクロを使ってエディタの内部情報を取得できるのですから、ちゃんとEmacsのマーク・リージョンの概念を実装した方がいいでしょう(ただ、ダイアログ等はこのマクロが効かないので、そのへんは外部のキーカスタマイズソフトのお世話になりたいところです)。マクロを使えばこれまで選択範囲を使って擬似的にしか表現できなかったマーク・リージョン操作が選択範囲やボタンの押し下げ状態に依存せず実現できることが分かります。

課題

以下にここで紹介したマクロの課題を挙げます。

マクロの作り方

今回初めて自分でVS.NETのマクロを作成したわけですが、最初は何を参考にすればいいかも分からず戸惑いました。そこでマクロを作る際のヒントを残しておきます。

簡単な方法はMSDN Libraryにも書いてありますが「ツール(T)」→「マクロ(M)」→「TemporaryMacro の記録(C)」を使ってマクロを記録し、その記録されたマクロ記述MyMacrosのRecordingModuleあたりを参考にして、希望の動作がどのように記述できるかを調べます。

大体はどこかのオブジェクトへのメソッド呼び出しになっているはずです。オブジェクト名やメソッド名をMSDN Libraryで引いて、理解を深めます。同じオブジェクトのほかのプロパティやメソッドに使えそうなものが無いか調べます。

そうして調べたプロパティやメソッド呼び出しをうまく制御するようなプログラムを組みます。Visual Basicの文法を知らなければならないので、言語仕様やサンプルマクロ(Samples)を参考にします。

MSDN Library内の参考になる部分を次に挙げます。