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メソッドは親クラスで定義されているので、オーバーライドする。