S-JIS[2013-08-21/2014-04-27] 変更履歴

Xtextリンク

Xtextのリンク(cross reference)のメモ。


概要

Xtextでも、別の箇所で定義された名前を参照するようにする(リンクを張る)ことが出来る。
それにより、以下の様な操作が出来るようになる。

Xtextの文法としては、以下の様にルール内容を角括弧で囲む。
縦棒で区切って型(終端ルール名)も指定できる。

ルール: 変数=[参照先ルール名];
ルール: 変数=[参照先ルール名|];

生成されたエディター上では、参照先ルールのnameに当たる部分が入力できるようだ。


リンクの例

5分チュートリアルにリンクを追加してみる。

Greeting:
	'Hello' name=ID ('from' ref=[Greeting])? '!';

あるいは

Greeting:
	'Hello' name=ID ('from' ref=[Greeting|ID])? '!';

参照先はGreetingで、その型はID。
Greetingのname部分が実際にエディター上で入力できるようなので、型は特に指定しなくてもよいだろう。


こうすると、以下の様なDSLが入力できる。

Hello foo !
Hello bar from foo !
Hello zzz from hoge !

そして、barのところにある「foo」にカーソルを合わせてF3キーを押すか、Ctrl+左クリックを押すことにより、fooの定義元にジャンプできる。
また、zzzのところにある「hoge」はどこにも定義されていないので、エラーになる(赤線が引かれる)。


なお、リンクはデフォルトではグローバルスコープになる。[2013-08-23]
つまり、別ファイルにある名前でも参照できる。


ローカルスコープの例

リンクはデフォルトではグローバルスコープになるので、限定された範囲だけリンク候補にしたい場合はローカルスコープを定義する。[2013-08-23]

参考: stackoverflowのXtext example of a scoped object


5分チュートリアルにブロックの定義を加えてみる。

xtextファイルのリンクの書き方はグローバルスコープでもローカルスコープでも変わらない。

Model:
	greetings+=Greeting*
	greeting+=Child*;

Greeting:
	'Hello' name=ID block=GreetingBlock '!';

GreetingBlock:
	'{' members+=Member (',' members+=Member)* '}';

Member:
	name=ID;

Child:
	'Child' name=ID 'extends' refName=[Greeting] block=ChildBlock '!';

ChildBlock:
	'{' members+=[Member] (',' members+=[Member])* '}';

Greetingでブロックを作り、Childでは親ブロックを指定して、Childのブロック内では他で定義されている名前のみ指定できる、という想定。

以下の様なDSLが入力できるはず。

Hello foo { f1 , f2 , f3 } !
Child bar extends foo { f1 , f3 } !

ここで、デフォルトのままだとグローバルスコープになってしまうので、ChildBlockのmembersをローカルスコープ (親ブロックで定義されているメンバーのみ指定できる)にしてみる。


スコープはIScopeProviderを実装したクラスで用意する。
「MyDsl」というDSLの場合、MyDslScopeProviderというクラスが自動生成されているので、ここに実装していく。
Xtext 2.4.2ではMyDslScopeProviderはXtendで書かれているが、自分はJavaの方が分かり易いので、Javaで書くことにする。

src/〜/MyDslScopeProvider.java:

package org.xtext.example.mydsl.scoping;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.scoping.IScope;
import org.eclipse.xtext.scoping.Scopes;
import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider;

import org.xtext.example.mydsl.myDsl.Child;
import org.xtext.example.mydsl.myDsl.ChildBlock;
import org.xtext.example.mydsl.myDsl.Greeting;
import org.xtext.example.mydsl.myDsl.Member;
public class MyDslScopeProvider extends AbstractDeclarativeScopeProvider {

	public IScope scope_ChildBlock_members(ChildBlock block, EReference reference) {
		// 親階層のオブジェクトを取得
		Child child = EcoreUtil2.getContainerOfType(block, Child.class);

		// 参照先オブジェクトを取得
		Greeting greeting = child.getRefName();

		// 参照先オブジェクトのブロック内のMember一覧を取得
		EList<Member> list = greeting.getBlock().getMembers();

		// スコープを返す
		return Scopes.scopeFor(list);
	}
}

メソッド名は「scope_」で始め、スコープを定義したいルール名と変数名を「_」でつなぐ。
例えばChildBlockのmembers変数をローカルスコープにしたい場合、メソッド名は「scope_ChildBlock_members」となる。
第1引数はルール定義のクラス(この例ではChildBlock)とする。

EcoreUtil2.getContainerOfType()で、自分を内包している(自分の外側の)ルールを取得する。
このメソッドは、型を指定することにより、何階層か上のルールでも検索してくれるので便利。

そこから参照先のオブジェクトを取得し、スコープ内に存在するメンバー一覧を取得する。
Scopes.scopeFor()でIScopeオブジェクトを作る。

Scopes.scopeFor()に渡すリストの要素の型は、xtextのリンク定義で指定した型になるようにするようだ。
つまりxtextで「hoge=[Zzz]」と定義している場合、Scopes.scopeFor()には「List<Zzz>」を渡す。

※入力補完の際には、このリストが入力候補一覧に出る。


ところで、Xtext 2.4.2では、Fooルールのzzz変数のスコープのメソッドを定義したいとき、Fooルールの外側ルール(例えばBarルールとする)を引数にしないといけない場合があるようだ。

	public IScope scope_Foo_zzz(Foo context, EReference reference) {
		〜
	}
	public IScope scope_Foo_zzz(Bar context, EReference reference) {
		〜
	}

バリデート時に本来の(上側の)メソッドが呼ばれ、入力補完時に下側のメソッドが呼ばれることがある模様。

これは、どうやら、入力補完時の入力状態によって変わるようだ。[2013-08-25]
まだ入力中はまだ内側のルールが確定していない(ルールオブジェクトが生成されていない)ことがあるので、そのときは外側のルールオブジェクトで呼ばれるっぽい。
極端に言えば、第1引数をEObjectにしておけばどの状態でも呼ばれるようになる。

	public IScope scope_Foo_zzz(EObject context, EReference reference) {
		〜
	}

ノードによる名称取得の例

ハイパーリンク(クロスリファレンス)を定義すると「そのルールに該当するEObject」から「参照先のEObject」が取得できるのだが、
記述されたDSLにエラーがある(ハイパーリンク部分がまだ書かれていない場合やハイパーリンクの参照先が無い)場合はnullが返ってくる。
ハイパーリンク先が存在しない場合でもDSL上には何かしらの文字列は記述されているわけで、これはINodeから取得できる。[2013-09-11]

import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;

import org.xtext.example.mydsl.myDsl.Child;
import org.xtext.example.mydsl.myDsl.Greeting;
import org.xtext.example.mydsl.myDsl.MyDslPackage;
	protected Object _text(Child child) {
		String name = child.getName();

		String refName = null;
		{
			Greeting ref = child.getRefName();
			if (ref != null) {
				refName = ref.getName();
			}
			if (refName == null) {
				List<INode> list = NodeModelUtils.findNodesForFeature(child, MyDslPackage.Literals.CHILD__REF_NAME);
				for (INode node : list) {
					refName = node.getText();
				}
			}
		}

		if (refName != null) {
			return String.format("%s extends %s", name, refName);
		} else {
			return name;
		}
	}

ルール定義外のハイパーリンクを作る例

Xtextのルール定義(xtextファイル内で角括弧を使う)以外でもハイパーリンクを作ることが出来る。[2014-04-27]

XtextのハイパーリンクはHyperlinkHelperというクラスで実装されているので、それをオーバーライドして独自のハイパーリンクを作ればよい。

参考: eclipse Labs(domainmodel example)のDomainmodelHyperlinkHelper.java


DMDLのモデル名からモデルクラス(Javaソース)へのハイパーリンクを作る例。

uiプロジェクト/src/〜/DMDLHyperlinkHelper.java:

import java.util.List;

import jp.hishidama.xtext.dmdl_editor.dmdl.DmdlPackage;
import jp.hishidama.xtext.dmdl_editor.dmdl.ModelDefinition;
import jp.hishidama.xtext.dmdl_editor.dmdl.ModelUiUtil;

import org.eclipse.emf.ecore.EObject;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jface.text.Region;
import org.eclipse.xtext.common.types.xtext.ui.JdtHyperlink;
import org.eclipse.xtext.common.types.xtext.ui.TypeAwareHyperlinkHelper;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.resource.EObjectAtOffsetHelper;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.resource.XtextResourceSet;
import org.eclipse.xtext.ui.editor.hyperlinking.IHyperlinkAcceptor;

import com.google.inject.Inject;
import com.google.inject.Provider;
public class DMDLHyperlinkHelper extends TypeAwareHyperlinkHelper {

	@Inject
	private EObjectAtOffsetHelper eObjectAtOffsetHelper;

	@Inject
	private Provider<JdtHyperlink> jdtHyperlinkProvider;
	@Override
	public void createHyperlinksByOffset(XtextResource resource, int offset, IHyperlinkAcceptor acceptor) {
		super.createHyperlinksByOffset(resource, offset, acceptor);

		INode node = getModelNameNode(resource, offset);
		if (node == null) {
			return;
		}
		String modelName = node.getText().trim();

		IJavaProject javaProject = getJavaProject(resource);
		if (javaProject == null) {
			return;
		}
		String className = ModelUiUtil.getModelClassName(javaProject.getProject(), modelName); //DMDLのモデル名からクラス名へ変換する
		if (className == null) {
			return;
		}
		try {
			IType type = javaProject.findType(className);
			if (type != null) {
				JdtHyperlink hyperlink = jdtHyperlinkProvider.get();
				hyperlink.setJavaElement(type);
				hyperlink.setTypeLabel("Open DataModel class");
				hyperlink.setHyperlinkText("Open DataModel class");
				hyperlink.setHyperlinkRegion(new Region(node.getOffset(), node.getLength()));
				acceptor.accept(hyperlink);
			}
		} catch (JavaModelException e) {
			return;
		}
	}

Javaソースへハイパーリンクジャンプする為のJdtHyperlinkクラスはXtextで用意されている。

	private INode getModelNameNode(XtextResource resource, int offset) {
		EObject object = eObjectAtOffsetHelper.resolveElementAt(resource, offset);
		if (object instanceof ModelDefinition) { //DMDLのモデル定義
			List<INode> list = NodeModelUtils.findNodesForFeature(object, DmdlPackage.Literals.MODEL_DEFINITION__NAME); //DMDLのモデル名
			if (!list.isEmpty()) {
				INode node = list.get(0);
				if (node.getOffset() <= offset && offset < node.getOffset() + node.getLength()) {
					return node;
				}
			}
		}
		return null;
	}
	private IJavaProject getJavaProject(XtextResource resource) {
		XtextResourceSet set = (XtextResourceSet) resource.getResourceSet();
		Object context = set.getClasspathURIContext();
		if (context instanceof IJavaProject) {
			return (IJavaProject) context;
		}
		return null;
	}
}

uiプロジェクト/src/〜/DMDLUiModule.java:

public class DMDLUiModule extends jp.hishidama.xtext.dmdl_editor.ui.AbstractDMDLUiModule {
〜
	@Override
	public Class<? extends IHyperlinkHelper> bindIHyperlinkHelper() {
		return DMDLHyperlinkHelper.class;
	}
}

bindIHyperlinkHelperメソッドは親クラスで定義されているので、オーバーライドする。


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