原文: http://yasnippet.googlecode.com/svn/trunk/doc/snippet-development.html (translated on 2010/Dec/21) ⇒ editor's room

Writing snippets

〔重要〕 このドキュメントは YASnippet の SVN trunk に対して適用されるものです。 SVN trunk はこちらから取得できます。 他のバージョンに対するドキュメントはこちらで見られます。

スニペットの開発

スニペットをすばやく見つける

スニペットファイルをすばやく見つける方法はいくつかあります:

M-x yas/new-snippet

スニペット名の入力プロンプトを開き、それからそのスニペットを置くのに適したディレクトリを推測し、そのディレクトリが存在しない場合は作成するか否かのプロンプトを表示します。 最後に、あなたがスニペットを書けるように、snippet-mode に設定された新しいバッファを開きます。

M-x yas/find-snippets

find-file-other-window と同様に、読み込み済みのスニペットが置いてあったディレクトリで(もしそれが存在したならば)、スニペットファイルを開きます。 ここで使われるディレクトリ検索ロジックは M-x yas/new-snippet に似ています。

M-x yas/visit-snippet-file

yas/insert-snippet と同様に、展開したいスニペットの名前を入力するプロンプトを表示します。 ただし、実際にそれを展開する代わりに、そのスニペットの定義ファイルに移動します(それが存在していれば)。

スニペットファイルが見つかると、バッファが後述する snippet-mode に設定され、スニペットの編集を始めることができます。

メジャーモード snippet-mode を使う

スニペットを編集するためのメジャーモード snippet-mode があります。 M-x snippet-mode とすることで、バッファをこのモードに設定することができます。 このモードは、それなりに有用な構文ハイライトを提供してくれます。

このモードでは、2個のコマンドが定義されています:

M-x yas/load-snippet-buffer

スニペットを編集しているとき、そのスニペットを現在のモードおよびメニューに読み込みます。 このコマンドは snippet-mode ではデフォルトで C-c C-c にキーバインドされています。

M-x yas/tryout-snippet

スニペットを編集しているとき、新しい空のバッファを開き、そのバッファを適切なメジャーモードに設定し、スニペットをそこに挿入します。 これにより、そのスニペットが実際にどんなふうに展開されるのかを確認することができます。 このコマンドは snippet-mode では C-c C-t にキーバインドされています。

スニペットを書くためのスニペットもあります: vars$f$m です :-)

ファイルの内容

スニペットを定義しているファイルには、通常、展開されるテンプレートが書かれています。

省略可能ですが、もしファイルに # -- という行が含まれていた場合、その行より上にある行はコメントとして扱われます。 それらの行の一部はディレクティブ(あるいはメタデータ)になることもあります。 スニペットのディレクティブは # property: value のような形式で、その後に書かれているスニペットの属性を微調整するものです。 もし、ファイルの中に # -- という行が含まれていない場合、ファイル全体がスニペットのテンプレートとみなされます。

以下に典型的な例を示します:

#contributor : pluskid <pluskid@gmail.com>
#name : __...__
# --
__${init}__

現在サポートされているディレクティブを以下に示します:

# key: スニペット省略形

これはおそらくもっとも重要なディレクティブです。 スニペットを展開するときに yas/trigger-key を押す前に入力する省略形です。

このディレクティブを指定しない場合、スニペットが書かれていたファイルの名前がデフォルト値として使われます。 ただし、YASnippet がファイル名をトリガーとみなさないように設定されている場合は別です(Organizing snippets の章yas/ignore-filenames-as-triggers のところを参照してください)。 もし YASnippet がファイル名をトリガーとみなさないように設定されていた場合は、snippet key の仕組みではそのスニペットを展開できないことになります。

snippet key が ASCII 文字以外であったり、ファイル名として妥当なものでなかったり(たとえばスラッシュ / を含むとか)することがあります。 そういったスニペットでは、この key 属性を定義することで、snippet key としてファイル名がそのまま使われないようにしてください。

# name: スニペット名

これはスニペットの一行だけの説明書きです。 この説明書きはメニューに表示されます。 よく似たスニペットを区別するためにも、 そのスニペットをうまく説明する名前を付けるとよいでしょう。

この名前を省略した場合、デフォルトでは、スニペットを読み込んだファイルの名前が使われます。

# condition: スニペットの展開条件

これは、一片の Emacs-lisp コードです。 スニペットが条件を持つ場合、その条件コードを評価して nil 以外の値になったときだけスニペットは展開されます。

Expanding snippets の章yas/buffer-local-condition のところも参照してください。

# group: スニペットメニューのグルーピング

メニューバーの項目からスニペットを展開しようとしたとき、ここで与えられたモードに対応するスニペットはサブメニューにグルーピングされます。 この機能は、あるモードに対するスニペットがあまりに多くて、メニューが長くなりすぎるようなときに便利です。

この # group: 属性は、メニューの構築にだけ影響します(the YASnippet menu の章を参照)。 スニペットをサブディレクトリに分類して、特殊ファイル .yas-make-groups を使うと、このディレクティブと同じ効果が得られます(Organizing Snippets の章を参照)。

この # group: ディレクティブの例としては、ruby-mode 向けに同梱されているスニペットを参照してください。 グループはネストすることも可能です。たとえば、control structure.loops という記述は、control structure グループの下にある loops グループの下にそのスニペットがあることを表します。

# expand-env: 展開する環境

これは、let varlist form の形をした一片の Emacs-lisp コードです。 すなわち、変数への値の割り当てのリストのリストです。 これは、スニペットが展開されるときの変数の値を上書きするために使うことができます。

上書きする意味がある変数としては、yas/wrap-around-regionyas/indent-line があります(Expanding Snippets の章を参照)。

例として、あなたは普段、yas/indent-line'auto に、yas/wrap-around-regiont に設定しているとします。 残念ながら、この、とりわけ見事なアスキーアートは、それらの設定によって台無しになってしまうことでしょう。 こんなとき、次のように使うのです:

# name : ASCII home
# expand-env: ((yas/indent-line 'fixed) (yas/wrap-around-region 'nil))
# --
                welcome to my
            X      humble
           / \      home,
          /   \      $0
         /     \
        /-------\
        |       |
        |  +-+  |
        |  | |  |
        +--+-+--+

# binding: 直接的なキーバインド

Emacs の通常のキーバインドでスニペットをダイレクトに展開するために、このディレクティブが使えます。 そのキーバインドは、そのスニペットが対象としているメジャーモードにちなんだ名前の Emacs keymap に登録されます。

さらに、変数 yas/prefix は、普段あなたがコマンドにつけているプレフィックスに設定されます。 これを使うと、ひとつのスニペットにちょっとしたバリエーションをつけられます。 たとえば、次の "html-mode" スニペットのように:

#name : <p>...</p>
#binding: "C-c C-c C-m"
# --
<p>`(when yas/prefix "\n")`$0`(when yas/prefix "\n")`</p>

この割り当ては、keymap html-mode-map に記録されます。 p タグを改行付きで展開したいときには、"C-u C-c C-c C-m" と押すだけです。 "C-u" を省くと、改行なしで p タグが展開されます。

メジャーモードの名前に基づく keymap の選択を上書きすることができます。 このディレクティブにコンスセルを指定してください。そのコンスセルの最初の要素を、キーバインドを記録させたい keymap の名前にします。

#name : <p>...</p>
#binding: (rinari-minor-mode-map . "C-c C-c C-m")
# --
<p>`(when yas/prefix "\n")`$0`(when yas/prefix "\n")`</p>

注: この機能はまだ実験的なものです。YASnippet の将来のリリースでは無くなったり変更されたりするかもしれません。 慎重にお使いください。 多くの基本的なモードで重要なキーバインドを上書きしてしまうのは容易で、元の定義に戻すのは困難です。 とりあえず、どのキーバインドが有効になっているかは、変数 yas/active-keybindings をみればわかります。 また、関数 yas/kill-snippet-keybindings は、すべてのキーバインドを取り消す(元に戻す)ことを試みてくれます。

# contributor: スニペットの作者

このディレクティブは付加的なもので、スニペットの機能にはなにも影響を与えませんが、見栄えがします。

テンプレートの構文

スニペットのテンプレートの構文は、シンプルですが強力です。TextMate の構文にとてもよく似ています。

プレインテキスト

テンプレートの中身としては、任意のテキストが書けます。 テキストは、$ および ` を除き、プレインテキストと解釈されます。 これらの文字をエスケープするにはバックスラッシュ(円記号)が必要です (\$ or \`)。 バックスラッシュ(円記号)それ自体のエスケープ、つまり \\ が必要な場合もあります。

埋め込まれた Emacs-lisp コード

バッククォート (`) で囲むことで、Emacs-Lisp のコードをテンプレート中に埋め込むことができます。 その lisp フォームはスニペットが展開されるときに評価されます。 その評価は、スニペットが展開されるバッファの中で(そのバッファの environment で)評価されます。

c-mode でヘッダファイルの二重読み込みガード文を動的に生成する、というサンプルを以下に示します:

#ifndef ${1:_`(upcase (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))`_H_}
#define $1

$0

#endif /* $1 */

YASnippet のバージョン 0.6 以降では、スニペットの展開は、ある特別な Emacs-lisp 変数の束縛のもとで行われます。 そのひとつは yas/selected-text です。 以下のようにスニペットを定義することができます:

for ($1;$2;$3) {
  `yas/selected-text`$0
}

このように定義すると、スニペットの中に選択領域を「包み込む」ことができます。 さらに言えば、変数 yas/wrap-around-regiont に設定することで、この動作を自動的に行うようにすることもできます。

タブストップ

タブストップというのは、TABS-TAB で行ったり来たりできるフィールドです。 タブストップは、記号 $ とそれに続く数字1個で記述します。 $0 は特別な意味を持っていて、スニペットの exit point を表します。 これは、すべてのフィールドを一巡したときに最後にたどりつく場所です。 典型的な例は以下の通りです:

<div$1>
    $0
</div>

プレースホルダー

タブストップは、デフォルト値をとることができます。それはプレースホルダーと呼ばれています。その文法は以下のようになります:

${N:default value}

これらはタブストップに対するデフォルト値となります。 ユーザがそのタブストップで初めて文字をタイプした時点で、そのデフォルト値はユーザのタイプした文字で置き換えられます。 そのフィールドに対して「ミラー」や「変換」を指定したいのでなければ、 数字の指定を省略することができます。

ミラー

プレースホルダーのついたタブストップは、ひとつのフィールドとみなすことができます。 ひとつのフィールドは1個または複数の「ミラー」を持つことができます。 ユーザがフィールドのテキストを書き換えると、そのフィールドのミラーが更新されます。 以下に例を示します:

\begin{${1:enumerate}}
    $0
\end{$1}

ユーザが ${1:enumerate} フィールドに "document" とタイプすると、 その "document" という単語が \end{$1} にも挿入されます。 良い解説動画があります: YouTube or avi video

同じ番号を付けられたタブストップは、ミラーとして機能します。 同じ番号を付けられたどのタブストップもデフォルト値を持っていない場合、スニペットファイル中で一番上に書かれているタブストップがフィールドとして選ばれ、それ以外はミラーとなります。

変換を伴うミラー

もし、プレースホルダー ${n: のデフォルト値が $( で始まる場合、 それはフィールド n に対する「変換を伴うミラー」と解釈されます。 ミラーのテキストは、「変換」にしたがって求められたものとなります。 この「変換」は Emacs-lisp コードであり、このコードは、変数 text (もしくは yas/text)がフィールド n の中身に束縛されている環境で評価されます。 以下に Objective-C 向けのサンプルを示します:

- (${1:id})${2:foo}
{
    return $2;
}

- (void)set${2:$(capitalize text)}:($1)aValue
{
    [$2 autorelease];
    $2 = [aValue retain];
}
$0

${2:$(capitalize text)} を見てみてください。 これは、フィールドではなく「変換を伴うミラー」です。 実際のフィールドは最初の行の ${2:foo} です。 ユーザが ${2:foo} にテキストを入力すると、 「変換」が評価され、 変換後のテキストとしてミラーに書かれます。 つまり、上記のサンプルでは、ユーザがフィールドに "baz" とタイプすると、変換後のテキストは "Baz" となります。 この例も上記の動画で見られます。

もうひとつ、rst-mode 向けのサンプルを示します。 reStructuredText では、上下を "===" で囲ったテキストがドキュメントのタイトルになります。 この "===" は、少なくともテキストと同じ長さでなければなりません。 つまり、

=====
Title
=====

は正しいタイトルですが、

===
Title
===

は正しくありません。 rst のタイトル部のためのスニペットは、以下のようになります:

${1:$(make-string (string-width text) ?\=)}
${1:Title}
${1:$(make-string (string-width text) ?\=)}

$0

変換を伴うフィールド

YASnippet のバージョン 0.6 以降では、フィールドの内部で lisp による変換を行うことができます。 この機能は「変換を伴うミラー」とほとんど同じように動きますが、その lisp コードの評価(値の変換)は次のタイミングで行われます。 すなわち、ユーザがそのフィールドに入ったとき、そのフィールドに変更を加えた時、そのフィールドから抜けるとき、このいずれのタイミングでも評価が行われます。

この機能の構文は、「変換を伴うミラー」とはわずかに違います。その違いは、パーサがフィールドとミラーを区別するためのものです。 次の例において:

#define "${1:mydefine$(upcase yas/text)}"

ユーザがこのフィールドに入ると、"mydefine" は自動的に "MYDEFINE" に変換されます。 ユーザがテキストを入力するたびに、この変換は行われます。

ひとつ注意してください。 この「変換を伴うフィールド」を「変換を伴うミラー」と区別するために、YASnippet はコロン (:) とドルマーク ($) の間に追加のテキストを必要とします(訳注:上記の例での "mydefine" のようにプレースホルダーを書いておく必要があります)。 もし、この追加のテキストを書きたくない場合、2個の $ を使ってください。

#define "${1:$$(upcase yas/text)}"

次のことにも注意してください。 変換が行われるとき、それと同時に、フィールドの値が変更され、内部的な「更新状態」が true に設定されます。 結果として、通常のフィールドに対して行われる「自動的な削除」は行われません。 この挙動は意図的なものです。

リストからのフィールドの値の選択、および他の技

これまで見てきたように、フィールドの値の変換は、ユーザがそのフィールドに入った後に行われ、またいくつかの便利な変数束縛(特に yas/field-modified-pyas/moving-away-p)の元で評価されます。 この特長を利用すると、あるフィールドに対して、そのデフォルト値を選択できるようにすることができます。

yas/choose-value はこの機能を提供します。 例:

<div align="${2:$$(yas/choose-value '("right" "center" "left"))}">
  $0
</div>

さきに挙げた2個の変数をどのように使って書かれているか、関数 yas/choose-value そのものの定義を見てみてください。

別の使い方を以下に示します。これは LaTeX-mode 向けのスニペットで、ユーザがフィールド 2 に移動したときに reftex-label を呼ぶ、というものです。 この例では yas/modified-p を直接使っています。

\section{${1:"Titel der Tour"}}%
\index{$1}%
\label{{2:"waiting for reftex-label call..."$(unless yas/modified-p (reftex-label nil 'dont-
insert))}}%

関数 yas/verify-value の定義では、もうひとつ別の巧みな技を使っていて、yas/moving-away-p を活用しています。 ぜひ実際に試してみてください。 こちらのスレッドもチェックしてみてください。

入れ子になったプレースホルダー

YASnippet のバージョン 0.6 以降では、入れ子になったプレースホルダーを使うことができます:

<div${1: id="${2:some_id}"}>$0</div>

この記述は、div 要素に id 属性を指定するかどうか、ユーザが選択できるようにしてくれます。 id 要素を展開した後に TAB を押すと、"some_id" を好きなものに書き換えられます。 あるいは、単に C-d を押せば(これは yas/skip-and-clear-or-delete-char を実行するキーバインドです)、 すぐに exit marker に進みます(訳注:タブストップ $0 に移動します)。

ちなみに、C-d は、カーソルがフィールドの先頭にあって、かつ、フィールドの値がまだ変更されていなかった場合に限り、そのフィールドをクリアします。 そうでなければ、C-d は通常の Emacs の機能である delete-char コマンドを実行します。

カスタム可能な変数

yas/trigger-key

この変数で指定されたキーは、関数 yas/minor-mode が有効な時に yas/expand に割り当てられます。

この変数の値は文字列で、read-kdb-macro によって Emacs 内部のキー表現に変換されるものです。

デフォルトの値は "TAB" です。

yas/next-field-key

この変数で指定されたキーは、スニペットがアクティブなときに次のフィールドに移動する操作になります。

この変数の値は文字列で、read-kdb-macro によって Emacs 内部のキー表現に変換されるものです。

キーのリストを与えることもできます。

デフォルトの値は "TAB" です。

yas/prev-field-key

この変数で指定されたキーは、スニペットがアクティブな時にひとつ前のフィールドに移動する操作になります。

この変数の値は文字列で、read-kdb-macro によって Emacs 内部のキー表現に変換されるものです。

キーのリストを与えることもできます。

デフォルトの値は ("<backtab>" "S-tab") です。

yas/skip-and-clear-key

この変数で指定されたキーは、現在アクティブなフィールドをクリアする操作になります。

この変数の値は文字列で、read-kdb-macro によって Emacs 内部のキー表現に変換されるものです。

キーのリストを与えることもできます。

デフォルトの値は "C-d" です。

yas/good-grace

もし nil でない場合、スニペット定義の中にインラインで書かれた Emacs-lisp の評価の途中でエラーを上げません(例外を投げません)。 その代わりに文字列 "[yas] error" を返します。

yas/indent-line

変数 yas/indent-line は、インデントを制御します。 デフォルトでは 'auto に束縛されていて、スニペットが挿入されるときに、その挿入先のバッファのモードにしたがってスニペットがインデントされるようになっています。

もうひとつの変数 yas/also-auto-indent-first-line は、nil でないときに、まさに変数名どおりの仕事をします(訳注:スニペットの1行目も自動的にインデントされます。この変数が nil のときは、スニペットの1行目はインデントされません)。 :-)

スニペットのテンプレートで「ハードコーディングされたインデント」を使いたいときは、この変数 yas/indent-linefixed に設定します。

スニペットごとにインデント制御を変えたいときは、ディレクティブ # expand-env: の節を参照してください。

YASnippet のもっと前のバージョンとの後方互換性のため、スニペットの中に $> と書くと、その行が (indent-according-to-mode) によってインデントされます。 この挙動は、yas/indent-line'auto 以外の値に設定されているときだけ有効になります。

for (${int i = 0}; ${i < 10}; ${++i})
{$>
$0$>
}$>

yas/wrap-around-region

もし nil でない値だった場合、YASnippet はスニペットの exit marker (タブストップ $0)の位置に現在の選択領域(のテキスト)を挿入します。 もしこの変数が t に設定されていた場合、$0 の位置に `yas/selected-text` とインラインで書いたのと同じ結果になります。 【訳は合ってると思います。2010/12/30 IKKI】

ほとんどの Emacs システムでは、文字入力を開始することで選択領域が削除されます。 そのため、この機能はたいてい yas/insert-snippet コマンドでスニペットを選択する場合に使われます。

また一方で、この変数の値が cua だった場合、YASnippet は、文字入力を開始することによって削除された選択領域のうちで最新のものを探して適用します。 この機能を使うと次のようなことが可能になります。まず領域を選択し、snippet key を入力し(この時点で選択領域は削除されます)、それから yas/trigger-key を押す、という操作によって、解除されてしまった選択領域をスニペットの中に復活させることが可能になります。

yas/triggers-in-field

もし nil でない値だった場合、yas/next-field-key は「積み重なった」展開のトリガーとなります。 「積み重なった」展開というのは、あるスニペットの展開が別のスニペットの展開を含むことを指します。 この変数 yas/triggers-in-field の値が nil だった場合は、yas/next-field-key は単に次のフィールドに移動するだけになります。

yas/snippet-revival

nil でない値だった場合、undo/redo のあとにスニペットのフィールドが再びアクティブになります。

yas/after-exit-snippet-hook および yas/before-expand-snippet-hook

これらのフックは、それぞれ、スニペットを挿入する前と、スニペットから抜けた後に、呼ばれます。【タイトルが「yas/after-exit-snippet-hook および yas/before-expand-snippet-hook」で本文が「スニペットを挿入する前と、スニペットから抜けた後」だと順序があべこべなので「それぞれ」とは言えないかも。タイトルを「yas/before-expand-snippet-hook および yas/after-exit-snippet-hook」にすべき? 2010/12/30 IKKI】 もし、これらのフックの奇妙で有効な使い道を見つけたら、それはおそらく YASnippet の設計上のミスなので、私たちに知らせてください。

TextMate スニペットの移植

TextMate の ".tmSnippet" XML ファイルを取り込んで YASnippet の定義を生成するためのツールが2個あります:

この章では、2番目のものについて手短にとりあげます。

textmate_import.rb と、あなたの使いたい TextMate バンドルを、ダウンロードしてください。

$ curl -O http://yasnippet.googlecode.com/svn/trunk/extras/textmate_import.rb
$ svn export http://svn.textmate.org/trunk/Bundles/HTML.tmbundle/

次に、以下のように textmate_import.rb を実行します:

$ ./textmate_import.rb -d HTML.tmbundle/Snippets/ -o html-mode -g HTML.tmbundle/info.plist

最終的に、html-mode サブディレクトリが得られ、その中には TextMate から移植されたスニペットが置かれているハズです。

$ tree html-mode # to view dir contents, if you have 'tree' installed

オプション -g は省略可能ですが、このツールがスニペットのグルーピングを把握する手助けになります。 Organizing Snippets の章にしたがって html-mode ディレクトリの中に .yas-make-groups.yas-ignore-filename-triggers を置くのを忘れないようにしてください。

指定可能な引数の一覧を見るには、textmate_import.rb --help と実行してみてください。

スニペットの移植はまだ完ぺきではないことに留意してください。 いくつかの(あるいは、たくさんの)スニペットで修正が必要かもしれません。 それらの修正をしたなら、google group に投稿してください。 もっと言うと、その修正を自動化できるように textmate_import.rb にパッチを当てて、そのパッチを投稿してください。