Xtextのアウトラインページのメモ。
Xtextのデフォルトで、エディター上に入力したDSLのアウトラインページが表示される。
また、Ctrl+oによるクイックアウトラインも表示される。
基本的に、xtextに書かれたルール毎に1つの階層になって、全体として階層構造で表示される。
ルール内にnameに代入している箇所があると、その名前がツリーに表示される。
nameが無いと「<unnamed>」と表示される。これを表示しないようにする為にはアウトラインページをカスタマイズする必要がある。
xtextファイルをビルドすると、アウトラインページクラスの空実装が作られる。
例えばDMDL.xtextの場合、uiプロジェクトにDMDLOutlineTreeProvider.xtendというファイルが作られる。(Xtext 2.4.2)
これはXtendという(Javaに似た)言語であり、ここにアウトラインページのカスタマイズを実装していく。
Xtext 2.4.2では、ソース整形クラスとして生成されるファイルはXtend用である。
個人的には新しい言語を覚えるよりJavaの方が分かり易いので、Javaで実装したい。
XtendはJavaに似ているので、Javaに変換するのも難しくない。
元のxtendファイルは、ファイル自体は削除せずに、中身を空にしておく。
中身が残っていると同じクラスがJavaとXtendの両方で定義されることになり、コンパイルエラーになる。
xtendファイル自体を消してしまうと、次にxtextファイルをビルドしたときにまた生成されてしまうので、ファイル自体は残しておく必要がある。
package jp.hishidama.xtext.dmdl_editor.ui.outline;
import org.eclipse.xtext.ui.editor.outline.impl.DefaultOutlineTreeProvider;
public class DMDLOutlineTreeProvider extends DefaultOutlineTreeProvider { }
デフォルトでは、アウトラインのツリーのルート要素はファイル名になっている。
これをファイル内のトップレベル要素に変更してみる。
デフォルト | こうしたい | |
---|---|---|
|
→ |
|
import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.ui.editor.outline.impl.DocumentRootNode;
@Override
protected void _createChildren(DocumentRootNode parentNode, EObject modelElement) {
for (EObject childElement : modelElement.eContents()) {
createNode(parentNode, childElement);
}
}
「_createChildren」のルート要素用のメソッドをオーバーライドする。(アウトラインクラスのメソッドは、何故か先頭に「_」が付いている)
デフォルトでは「createNode(parentNode, modelElement)
」を呼び出しているので、modelElementそのものでなく、modelElementの子要素でノードを作成してやればよい。
nameに代入していないルールに該当する階層は、「<unnamed>」と表示される。
unnamedを自動的に削除してくれる機能は無いので、不要な階層(ルール)はスキップするようにコーディングする必要がある。
階層にしないルールがきたら、そのルールの子要素でノードを作るようにしてやる。
import com.example.xtext.mydsl.MyListElement; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.ui.editor.outline.IOutlineNode;
//MyListElementというルールをスキップする例 @Override protected void _createChildren(IOutlineNode parentNode, EObject modelElement) { if (modelElement instanceof MyListElement) { MyListElement myList = (MyListElement)modelElement; for (EObject childElement : myList.getElements()) { createNode(parentNode, childElement); } return; } super._createChildren(parentNode, modelElement); }
ところで、実際に作り始めるとルールが結構な数になるので、いちいちif文でinstanceofを書くのが大変になってくる。
Xtextでは、ルールクラス毎にメソッドが作れるようになっている。
//MyListElementというルールをスキップする例 protected void _createChildren(IOutlineNode parentNode, MyListElement myList) { for (EObject childElement : myList.getElements()) { createNode(parentNode, childElement); } }
メソッド名は同一で、引数のEObjectを自分のルールクラスにするだけでよい。(@Overrideは付けない)
ルール定義が他のルール定義を含んでいる場合、それも階層として表示される。[2013-09-14]
それ以上下の階層を表示しないようにするには、ルール定義を「葉」にすればよい。
//Attributeというルールを葉にする例 protected boolean _isLeaf(Attribute modelElement) { return true; }
ツリーの要素に表示されるラベルは、デフォルトではルール定義内のnameに代入された値となる。
(LabelProviderが定義されている場合、そこで指定したラベルになる)
アウトラインページ独自のラベルを定義するには、_text()メソッドを実装する。
これも_text(Object modelElement)をオーバーライドしてinstanceofでルールを判定する方法で出来るが、
_createChildrenと同様に各メソッドを直接作ることが出来る。
//PropertyDefinitionというルールのラベルを変える例 protected Object _text(PropertyDefinition p) { String name = p.getName(); String type = p.getType(); return String.format("%s : %s", name, type); }
JDTのJavaクラスのアウトラインページでは、フィールドの型等に薄い色が付いている。
これを実現するには、Eclipseプラグインのアウトラインページで実現する方法と同じくStyledStringを使う。
(ただ、Eclipseプラグインでの編集方法よりXtextの方が分かり易い)
import org.eclipse.jface.viewers.StyledString;
protected Object _text(PropertyDefinition p) { String name = p.getName(); String type = p.getType(); StyledString ss = new StyledString(name); ss.append(" : " + type, StyledString.DECORATIONS_STYLER); return ss; }
ツリーの要素のデフォルトのアイコン(画像)は、LabelProviderが定義されている場合、そこで指定したアイコンになる。
アウトラインページ独自のアイコンを定義するには、_image()メソッドを実装する。
これも_image(Object modelElement)をオーバーライドしてinstanceofでルールを判定する方法で出来るが、
_textと同様に各メソッドを直接作ることが出来る。
import org.eclipse.jdt.ui.ISharedImages; import org.eclipse.jdt.ui.JavaUI; import org.eclipse.swt.graphics.Image;
//ModelDefinitionというルールのアイコンを変える例 protected Image _image(ModelDefinition model) { return JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CLASS); }
リソース(画像ファイル)からアイコンを取得する場合は、以下の様にするらしい。
import org.eclipse.xtext.ui.IImageHelper; import com.google.inject.Inject;
@Inject private IImageHelper imageHelper;
protected Image _image(DataType d) { return imageHelper.getImage("DataType.gif"); }
ルール定義で参照(クロスリファレンス)としたオブジェクトの場合もアウトラインを表示することが出来るが、
特に何もしないと、アウトライン上のツリー要素をクリックするとエディター上で参照先が選択されてしまう。[2013-10-09]
例として、DMDLエディターのGroupingルールをアウトラインに表示してみる。
Groupingルールは参照先の名前の一覧となっている。
Grouping: '%' name+=[Property|Name] (',' name+=[Property|Name])*;
見て分かる通り、name(Propertyの参照)の一覧を指定するようになっている。
import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.swt.graphics.Image; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.ui.editor.outline.impl.EStructuralFeatureNode; import org.eclipse.xtext.util.TextRegion;
protected boolean _isLeaf(Grouping group) { return false; }
protected void _createChildren(IOutlineNode parentNode, Grouping group) {
EStructuralFeature feature = DmdlPackage.Literals.GROUPING__NAME;
Image image = 〜;
List<INode> list = NodeModelUtils.findNodesForFeature(group, feature);
for (INode node : list) {
//× String text = node.getText();
String text = NodeModelUtils.getTokenText(node).trim();
EStructuralFeatureNode snode = createEStructuralFeatureNode(parentNode, group, feature, image, text, true);
TextRegion region = new TextRegion(node.getOffset(), node.getLength());
snode.setTextRegion(region);
}
}
NodeModelUtils.findNodesForFeature()でgroupルール内のnameのINode一覧を取得する。
これがGroupingに指定された名前一覧を表す。
createEStructuralFeatureNode()でアウトラインのツリーのノード(要素)を作成する。
この中で、指定したEObject(今回の例ではgroup)の位置が“クリック時の選択範囲”として算出される。
そのままだとツリーがクリックされたらgroupの位置(複数の名前が指定可能なので、その全体)が選択されてしまうので、個々の名前を選択するようにしたい。
そこで、個々のnameを表しているINodeの位置(region)で選択範囲を指定し直す。
ノードのテキストを取得するには、node.getText()
よりもNodeModelUtils.getTokenText(node)
を使った方が良い。[2013-10-15]
node.getText()
だと、前後の改行・空白やコメントも含まれてしまう。
NodeModelUtils.getTokenText(node)
なら、コメントは空白に置き換えられるので、trim()してやれば空白類を除去できる。