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で書くことにする。
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ソース)へのハイパーリンクを作る例。
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; } }
public class DMDLUiModule extends jp.hishidama.xtext.dmdl_editor.ui.AbstractDMDLUiModule { 〜
@Override public Class<? extends IHyperlinkHelper> bindIHyperlinkHelper() { return DMDLHyperlinkHelper.class; } }
bindIHyperlinkHelperメソッドは親クラスで定義されているので、オーバーライドする。