S-JIS[2013-08-18/2013-10-15] 変更履歴

Xtextアウトライン

Xtextのアウトラインページのメモ。


概要

Xtextのデフォルトで、エディター上に入力したDSLのアウトラインページが表示される。
また、Ctrl+oによるクイックアウトラインも表示される。

基本的に、xtextに書かれたルール毎に1つの階層になって、全体として階層構造で表示される。
ルール内にnameに代入している箇所があると、その名前がツリーに表示される。
nameが無いと「<unnamed>」と表示される。これを表示しないようにする為にはアウトラインページをカスタマイズする必要がある。

xtextファイルをビルドすると、アウトラインページクラスの空実装が作られる。
例えばDMDL.xtextの場合、uiプロジェクトにDMDLOutlineTreeProvider.xtendというファイルが作られる。(Xtext 2.4.2)
これはXtendという(Javaに似た)言語であり、ここにアウトラインページのカスタマイズを実装していく。


Javaソースへの変換

Xtext 2.4.2では、ソース整形クラスとして生成されるファイルはXtend用である。
個人的には新しい言語を覚えるよりJavaの方が分かり易いので、Javaで実装したい。

XtendはJavaに似ているので、Javaに変換するのも難しくない。

元のxtendファイルは、ファイル自体は削除せずに、中身を空にしておく。
中身が残っていると同じクラスがJavaとXtendの両方で定義されることになり、コンパイルエラーになる。
xtendファイル自体を消してしまうと、次にxtextファイルをビルドしたときにまた生成されてしまうので、ファイル自体は残しておく必要がある。

DMDLOutlineTreeProvider.java(DMDLの例):

package jp.hishidama.xtext.dmdl_editor.ui.outline;
import org.eclipse.xtext.ui.editor.outline.impl.DefaultOutlineTreeProvider;
public class DMDLOutlineTreeProvider extends DefaultOutlineTreeProvider {
}

ルート要素の変更

デフォルトでは、アウトラインのツリーのルート要素はファイル名になっている。
これをファイル内のトップレベル要素に変更してみる。

デフォルト   こうしたい
  • ファイル名
    • モデル1
    • モデル2
    • モデル3
  • モデル1
  • モデル2
  • モデル3
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");
	}

参考: Customizing Xtext


参照のアウトライン

ルール定義で参照(クロスリファレンス)としたオブジェクトの場合もアウトラインを表示することが出来るが、
特に何もしないと、アウトライン上のツリー要素をクリックするとエディター上で参照先が選択されてしまう。[2013-10-09]

Groupingというリンクをアウトラインに表示する例

例として、DMDLエディターのGroupingルールをアウトラインに表示してみる。
Groupingルールは参照先の名前の一覧となっている。

DMDL.xtext:

Grouping:
	'%' name+=[Property|Name] (',' name+=[Property|Name])*;

見て分かる通り、name(Propertyの参照)の一覧を指定するようになっている。

DMDLOutlineTreeProvider.java:

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()してやれば空白類を除去できる。


Xtext目次へ戻る / Eclipseへ戻る / 技術メモへ戻る
メールの送信先:ひしだま