S-JIS[2013-01-26/2013-05-20] 変更履歴

EclipseプラグインDMDLエディター(ハイパーリンク)

Eclipseプラグイン開発自作DMDLエディターでハイパーリンクを表示・クリックできるようにしてみる。


概要

ハイパーリンクは、エディター上でCtrlキーを押しながらマウスを動かすとマウスカーソルの位置の単語がハイパーリンクになって、クリックすると何らかの処理が行われるやつ。
Javaエディターで言えば、変数名をCtrl+クリックするとその変数の定義箇所にジャンプする。


DMDLエディターでも、別モデルやプロパティーを参照している箇所をCtrl+クリックしたら(Javaエディターと同様に)定義箇所にジャンプするようにしたい。

DMDLHyperlink.java

最初に、ハイパーリンクの範囲を保持し、クリックされたらジャンプする処理を作成する。

import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.IHyperlink;
public class DMDLHyperlink implements IHyperlink {
	private DMDLEditor editor;

	/** ハイパーリンク対象の単語 */
	private DMDLToken token;

	public DMDLHyperlink(DMDLEditor editor, DMDLToken token) {
		this.editor = editor;
		this.token = token;
	}

DMDLTokenは自作のパーサーによって解釈された単語を表す。
今回はこの単語がハイパーリンクの対象となる。

	@Override
	public IRegion getHyperlinkRegion() {
		int offset = token.getStart();
		int length = token.getLength();
		return new Region(offset, length);
	}

getHyperlinkRegion()でハイパーリンクの範囲を返す。
(このoffsetやlengthはIDocument上の位置・長さを表す)

	@Override
	public String getTypeLabel() {
		return null;
	}

	@Override
	public String getHyperlinkText() {
		return null;
	}
	@Override
	public void open() {
		DMDLToken target = token.getReference(); //データモデルやプロパティーが定義されている箇所を探す
		if (target != null) {
			int offset = target.getStart();
			int length = target.getLength();
			editor.selectAndReveal(offset, length);
		}
	}
}

open()には Ctrl+クリックされた際に行う処理を記述する。

DMDLエディターでは、単語の定義元を探し、見つかったらエディター上のその範囲を選択する。(結果として、カーソルが移動する)

マーカーを使ってジャンプする方法


DMDLHyperlinkDetector.java

次に、ハイパーリンク箇所を決定するクラスを作成する。

import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
public class DMDLHyperlinkDetector implements IHyperlinkDetector {
	protected DMDLEditor editor;
	public void init(DMDLEditor editor) {
		this.editor = editor;
	}
	@Override
	public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
		// TODO 毎回パースするのは無駄なので、なんとかしたい
		IDocument document = textViewer.getDocument();
		DMDLSimpleParser parser = new DMDLSimpleParser();
		ModelList models = parser.parse(document);

		// 指定された位置に該当する、独自解析ツリー上のトークンを取得する
		DMDLToken token = models.getTokenByOffset(region.getOffset());
		if (token != null && token instanceof WordToken) {
			WordToken word = (WordToken) token;
			switch (word.getWordType()) {
			case REF_MODEL_NAME:    //外部モデル名を参照している単語
			case REF_PROPERTY_NAME: //外部プロパティー名を参照している単語
				return new IHyperlink[] { new DMDLHyperlink(editor, token) };
			default:
				break;
			}
		}

		return null;
	}
}

detectHyperlinks()の第2引数regionで、エディター上でクリックされた範囲(だと思う)が入ってくる。
これを元にハイパーリンクすべきかどうかを判定し、ハイパーリンクの範囲を決定する。
そしてその範囲を表すIHyperlinkオブジェクトを作成して返す。

DMDLHyperlinkでeditorを使うので、DMDLHyperlinkDetectorでもeditorを受け渡す為のinitメソッドを用意した。


DMDLConfiguration.java

DMDLHyperlinkDetectorの初期化はDMDLConfigurationで行う。

import java.util.Arrays;

import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
public class DMDLConfiguration extends SourceViewerConfiguration {
〜
	private DMDLHyperlinkDetector hyperlinkDetector;

	public DMDLHyperlinkDetector getHyperlinkDetector() {
		if (hyperlinkDetector == null) {
			hyperlinkDetector = new DMDLHyperlinkDetector();
		}
		return hyperlinkDetector;
	}
	@Override
	public IHyperlinkDetector[] getHyperlinkDetectors(ISourceViewer sourceViewer) {
		IHyperlinkDetector[] ds = super.getHyperlinkDetectors(sourceViewer);
		IHyperlinkDetector[] ds2 = Arrays.copyOf(ds, ds.length + 1);
		ds2[ds2.length - 1] = getHyperlinkDetector();
		return ds2;
	}

getHyperlinkDetectors()で、親クラスのメソッドが返すHyperlinkDetector配列に自分のHyperlinkDetectorを追加して返す。

DMDLHyperlinkDetectorインスタンスを生成するメソッドをわざわざ用意しているのは、DMDLEditorから初期化の為に呼び出す必要があるから。


DMDLEditor.java

import jp.hishidama.eclipse_plugin.dmdl_editor.editors.hyperlink.DMDLHyperlinkDetector;

import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.PartInitException;
public class DMDLEditor extends TextEditor implements IPropertyChangeListener {
〜
	@Override
	public void init(IEditorSite site, IEditorInput input) throws PartInitException {
		super.init(site, input);

		DMDLConfiguration configuration = (DMDLConfiguration) getSourceViewerConfiguration();
		DMDLHyperlinkDetector detector = configuration.getHyperlinkDetector();
		detector.init(this);
	}

DMDLEditorの初期処理にて、DMDLHyperlinkDetectorの初期処理(DMDLEditorインスタンスを渡す)を行う。


F3キーによるジャンプ

JavaエディターではF3キーによって定義へジャンプ(Open Declaration)することが出来る。[2013-02-17]
ハイパーリンクでもこれと同様にF3キーを押したときにリンク先へジャンプしたい。

しかし単純にF3キーのバインディングを定義すると、Javaの「Open Declaration」とバッティングして、ポップアップメニューが出て「Open Declaration」と自分独自のジャンプを選択しなければならず、不便。
そこで 独自のコンテキストを定義してやると、自分のキーだけ有効になる。

参考: shin1ogawaさんの自作EditorにKeyBindを追加する

plulgin.xml:

   <extension
         point="org.eclipse.ui.contexts">
      <context
            id="dmdl-editor-plugin.context"
            name="DMDL Editor context"
            parentId="org.eclipse.ui.contexts.window">
      </context>
   </extension>
   <extension
         point="org.eclipse.ui.commands">
〜
      <command
            categoryId="dmdl-editor-plugin.category"
            id="dmdl-editor-plugin.jumplink-command"
            name="DMDL open declaration">
      </command>
   </extension>
   <extension
         point="org.eclipse.ui.handlers">
〜
      <handler
            class="jp.hishidama.eclipse_plugin.dmdl_editor.editors.hyperlink.DMDLHyperlinkHandler"
            commandId="dmdl-editor-plugin.jumplink-command">
      </handler>
   </extension>
   <extension
         point="org.eclipse.ui.bindings">
〜
      <key
            commandId="dmdl-editor-plugin.jumplink-command"
            contextId="dmdl-editor-plugin.context"
            schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
            sequence="F3">
      </key>
   </extension>

キーバインドでcontextIdを指定することにより、そのコンテキストの時だけキーが有効になる。
(デフォルトのコンテキストはorg.eclipse.ui.contexts.windowなので、コンテキスト定義の親コンテキストにはそれを指定している)

コマンドの定義方法


で、自分のエディターがそのコンテキストであることを指定する。(TextEditorとcontextIdを紐付ける)

DMDLEditor.java:

public class DMDLEditor extends TextEditor {
〜
	@Override
	protected void initializeKeyBindingScopes() {
		//super.initializeKeyBindingScopes();

		setKeyBindingScopes(new String[] { "org.eclipse.ui.textEditorScope", "dmdl-editor-plugin.context" });
	}
}

setKeyBindingScopes()は上書き(前の設定が保持されない)らしく、親クラスのinitializeKeyBindingScopes()を呼んでも意味が無い。[2013-05-20]
親クラスのinitializeKeyBindingScopes()で設定される「org.eclipse.ui.textEditorScope」は自分で設定に加える必要がある。
(「org.eclipse.ui.textEditorScope」を入れておかないと、ユーザーが定義した(一般的な)テキストエディター用のキーバインドが有効にならない)


ジャンプする処理自体はDMDLHyperlinkDetectorをそのまま使いたかったが、detectHyperlinks()の第1引数のITextViewerが単純には取得できない。
(TextEditor#getSourceViewer()で取れるのだが、protected finalメソッドなので、ちょっと面倒)
なので、DMDLHyperlinkだけ再利用した。

DMDLHyperlinkHandler.java:

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ui.handlers.HandlerUtil;
public class DMDLHyperlinkHandler extends AbstractHandler {
	@Override
	public Object execute(ExecutionEvent event) throws ExecutionException {
		DMDLEditor editor = (DMDLEditor) HandlerUtil.getActiveEditor(event);
		DMDLDocument document = editor.getDocument();
		ModelList models = document.getModelList();

		ITextSelection selection = (ITextSelection) editor.getSelectionProvider().getSelection();
		int offset = selection.getOffset();
		DMDLToken token = models.getTokenByOffset(offset);
		if (token != null && token instanceof WordToken) {
			WordToken word = (WordToken) token;
			switch (word.getWordType()) {
			case REF_MODEL_NAME:
			case REF_PROPERTY_NAME:
				DMDLHyperlink link = new DMDLHyperlink(editor, token);
				link.open();
				break;
			default:
				break;
			}
		}

		return null;
	}
}

HandlerUtil.getCurrentSelection()getShowInSelection()だとカーソル位置の正確な場所が取れなかったので、editorから位置を取得した。
そしてハイパーリンク対象かどうかを判定し(判定ロジック自体はDMDLHyperlinkDetectorと同様)、
DMDLHyperlink#open()を呼び出してジャンプする。


hyperlinkDetectorTargetsの例

上記の方法ではHyperlinkDetectorのインスタンス生成を自分で行ったが、plugin.xmlに定義を書く方法もある。[2013-02-24]

Javaエディターに対して独自のハイパーリンクを付ける場合は、plugin.xmlにhyperlinkDetectorの定義を行うだけだった。
これと同様にHyperlinkDetectorのクラスをplugin.xmlで書くようにすれば、他人のHyperlinkDetector定義も使えるようになるはず。そんな定義を書く人がいるかどうかは別として(笑)

その為には独自のtargetIdを定義しなければならない。
(JavaエディターのtargetIdは「org.eclipse.jdt.ui.javaCode」)

※ちなみに、この修正を行ったら(Ctrlキーを押さなくても)マウスカーソルを単語に当てるだけでリンクが表示されるようになった^^;


hyperlinkDetectorTargetsの定義

hyperlinkDetectorのtargetIdで指定する為の独自のターゲットを定義する。

plugin.xml:

   <extension
         point="org.eclipse.ui.workbench.texteditor.hyperlinkDetectorTargets">
      <target
            id="dmdl-editor-plugin.hyperlink.target"
            name="DMDL Editor hyperlink target">
         <context type="jp.hishidama.eclipse_plugin.dmdl_editor.editors.DMDLEditor"/>
      </target>
   </extension>

targetIdと、それに紐付くエディタークラスを指定する。


hyperlinkDetectorの定義

HyperlinkDetectorのクラスは以下のようにして指定することになる。

plugin.xml:

   <extension
         point="org.eclipse.ui.workbench.texteditor.hyperlinkDetectors">
      <hyperlinkDetector
            activate="true"
            class="jp.hishidama.eclipse_plugin.dmdl_editor.editors.hyperlink.DMDLHyperlinkDetector"
            id="dmdl-editor-plugin.hyperlinkDetector.open-declaration"
            name="Open Declaration"
            targetId="dmdl-editor-plugin.hyperlink.target">
      </hyperlinkDetector>
〜
   </extension>

HyperlinkDetectorの変更

HyperlinkDetectorクラスも若干修正する必要がある。

IHyperlinkDetectorインタフェースを直接実装するのではなく、AbstractHyperlinkDetectorを継承するようにする。
(当クラスが使われる過程でAbstractHyperlinkDetectorにキャストされている箇所があるから!)

また、インスタンス生成(初期化)が自分で制御できなくなるので、最初の方式のようにinit()DMDLEditorから呼び出して初期化することが出来ない。
幸い、AbstractHyperlinkDetectorはIAdaptableを扱えるようになっている(getAdapter()がある)ので、それを使って必要な情報を取ってくることにする。

DMDLHyperlinkDetector.java:

修正前 修正後 修正内容
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
 
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
インポートするクラスの変更。
public class DMDLHyperlinkDetector
	implements IHyperlinkDetector {
 
public class DMDLHyperlinkDetector
	extends AbstractHyperlinkDetector {
継承元クラスの変更。
    protected DMDLEditor editor;

    public void init(DMDLEditor editor) {
        this.editor = editor;
    }
    editorを保持するのを廃止。
   
        DMDLEditor editor =
            (DMDLEditor) getAdapter(DMDLEditor.class);
アダプターを使ってeditorを取得。

SourceViewerConfigurationの変更

HyperlinkDetectorクラスの修正に伴い、SourceViewerConfiguration実装クラスも変更する。

実はSourceViewerConfiguration自体にはhyperlinkDetectorTargetを扱う機能は無い。
SourceViewerConfigurationのサブクラスであるTextSourceViewerConfigurationを使う必要がある。

TextSourceViewerConfigurationでは設定ストアを使って「ハイパーリンクを有効にするかどうか」を判定するようになっている為、コンストラクターで設定ストアを渡す必要がある。
また、設定ストアのデフォルトではハイパーリンクは無効になっているので、設定ストアの初期値で有効にしてやる必要がある。

DMDLConfiguration.java:

修正前 修正後 修正内容
import org.eclipse.jface.text.source.SourceViewerConfiguration;
 
import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;

import org.eclipse.core.runtime.IAdaptable;
インポートするクラスの変更。
public class DMDLConfiguration
	extends SourceViewerConfiguration {
 
public class DMDLConfiguration
	extends TextSourceViewerConfiguration {
継承元クラスの変更。
    public DMDLConfiguration(DMDLEditor editor) {

        this.editor = editor;
    }
 
    public DMDLConfiguration(DMDLEditor editor) {
        super(Activator.getDefault().getPreferenceStore());
        this.editor = editor;
    }
親コンストラクター呼び出しの変更。
    private DMDLHyperlinkDetector hyperlinkDetector;

    public DMDLHyperlinkDetector getHyperlinkDetector() {
        if (hyperlinkDetector == null) {
            hyperlinkDetector = new DMDLHyperlinkDetector();
        }
        return hyperlinkDetector;
    }
    @Override
    public IHyperlinkDetector[] getHyperlinkDetectors(ISourceViewer sourceViewer) {
        IHyperlinkDetector[] ds = super.getHyperlinkDetectors(sourceViewer);
        IHyperlinkDetector[] ds2 = Arrays.copyOf(ds, ds.length + 1);
        ds2[ds2.length - 1] = getHyperlinkDetector();
        return ds2;
    }
    hyperlinkDetectorのインスタンス生成処理を廃止。
   
    public static final String HYPERLINK_TARGET_ID =
	"dmdl-editor-plugin.hyperlink.target";

    @Override
    protected Map<String, IAdaptable> getHyperlinkDetectorTargets(
	ISourceViewer sourceViewer) {

        @SuppressWarnings("unchecked")
        Map<String, IAdaptable> targets = super
        	.getHyperlinkDetectorTargets(sourceViewer);

        targets.put(HYPERLINK_TARGET_ID, editor);

        return targets;
    }
targetIdとエディターインスタンスの紐付け処理を追加。

DMDLEditorPreferenceInitializer.java:

import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
	@Override
	public void initializeDefaultPreferences() {
		IPreferenceStore store = Activator.getDefault().getPreferenceStore();
〜
		// Hyperlink
		store.setDefault(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_HYPERLINKS_ENABLED, true); //ハイパーリンクを有効にする
	}

TextEditorの修正

最後に、自分のエディターを修正する。

DMDLHyperlinkDetectorの初期処理(init()呼び出し)は不要になるので削除する。
DMDLHyperlinkDetectorでgetAdapter()からエディターを取得できるようにする必要があるので、その処理を追加する。

DMDLEditor.java:

修正前 修正後 修正内容
    @Override
    public void init(IEditorSite site, IEditorInput input)
	throws PartInitException {
        super.init(site, input);

        DMDLConfiguration configuration =
        	(DMDLConfiguration) getSourceViewerConfiguration();
        DMDLHyperlinkDetector detector =
        	configuration.getHyperlinkDetector();
        detector.init(this);
    }
    HyperlinkDetectorの初期処理を廃止。
    @Override
    public Object getAdapter(Class adapter) {



        〜
    }
 
    @Override
    public Object getAdapter(Class adapter) {
        if (adapter.isInstance(this)) {
            return this;
        }
        〜
    }
自分自身を返す処理を追加。

Eclipseプラグインへ戻る / Eclipseへ戻る / 技術メモへ戻る
メールの送信先:ひしだま