Eclipseのプラグイン開発の自作DMDLエディターでハイパーリンクを表示・クリックできるようにしてみる。
|
ハイパーリンクは、エディター上でCtrlキーを押しながらマウスを動かすとマウスカーソルの位置の単語がハイパーリンクになって、クリックすると何らかの処理が行われるやつ。
Javaエディターで言えば、変数名をCtrl+クリックするとその変数の定義箇所にジャンプする。
DMDLエディターでも、別モデルやプロパティーを参照している箇所をCtrl+クリックしたら(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エディターでは、単語の定義元を探し、見つかったらエディター上のその範囲を選択する。(結果として、カーソルが移動する)
次に、ハイパーリンク箇所を決定するクラスを作成する。
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メソッドを用意した。
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から初期化の為に呼び出す必要があるから。
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インスタンスを渡す)を行う。
JavaエディターではF3キーによって定義へジャンプ(Open Declaration)することが出来る。[2013-02-17]
ハイパーリンクでもこれと同様にF3キーを押したときにリンク先へジャンプしたい。
しかし単純にF3キーのバインディングを定義すると、Javaの「Open Declaration」とバッティングして、ポップアップメニューが出て「Open
Declaration」と自分独自のジャンプを選択しなければならず、不便。
そこで 独自のコンテキストを定義してやると、自分のキーだけ有効になる。
参考: shin1ogawaさんの自作EditorにKeyBindを追加する
<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を紐付ける)
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だけ再利用した。
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()を呼び出してジャンプする。
上記の方法ではHyperlinkDetectorのインスタンス生成を自分で行ったが、plugin.xmlに定義を書く方法もある。[2013-02-24]
Javaエディターに対して独自のハイパーリンクを付ける場合は、plugin.xmlにhyperlinkDetectorの定義を行うだけだった。
これと同様にHyperlinkDetectorのクラスをplugin.xmlで書くようにすれば、他人のHyperlinkDetector定義も使えるようになるはず。そんな定義を書く人がいるかどうかは別として(笑)
その為には独自のtargetIdを定義しなければならない。
(JavaエディターのtargetIdは「org.eclipse.jdt.ui.javaCode」)
※ちなみに、この修正を行ったら(Ctrlキーを押さなくても)マウスカーソルを単語に当てるだけでリンクが表示されるようになった^^;
hyperlinkDetectorのtargetIdで指定する為の独自のターゲットを定義する。
<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のクラスは以下のようにして指定することになる。
<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クラスも若干修正する必要がある。
IHyperlinkDetectorインタフェースを直接実装するのではなく、AbstractHyperlinkDetectorを継承するようにする。
(当クラスが使われる過程でAbstractHyperlinkDetectorにキャストされている箇所があるから!)
また、インスタンス生成(初期化)が自分で制御できなくなるので、最初の方式のようにinit()をDMDLEditorから呼び出して初期化することが出来ない。
幸い、AbstractHyperlinkDetectorはIAdaptableを扱えるようになっている(getAdapter()がある)ので、それを使って必要な情報を取ってくることにする。
修正前 | → | 修正後 | 修正内容 |
---|---|---|---|
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を取得。 |
HyperlinkDetectorクラスの修正に伴い、SourceViewerConfiguration実装クラスも変更する。
実はSourceViewerConfiguration自体にはhyperlinkDetectorTargetを扱う機能は無い。
SourceViewerConfigurationのサブクラスであるTextSourceViewerConfigurationを使う必要がある。
TextSourceViewerConfigurationでは設定ストアを使って「ハイパーリンクを有効にするかどうか」を判定するようになっている為、コンストラクターで設定ストアを渡す必要がある。
また、設定ストアのデフォルトではハイパーリンクは無効になっているので、設定ストアの初期値で有効にしてやる必要がある。
修正前 | → | 修正後 | 修正内容 |
---|---|---|---|
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とエディターインスタンスの紐付け処理を追加。 |
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
@Override
public void initializeDefaultPreferences() {
IPreferenceStore store = Activator.getDefault().getPreferenceStore();
〜
// Hyperlink
store.setDefault(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_HYPERLINKS_ENABLED, true); //ハイパーリンクを有効にする
}
最後に、自分のエディターを修正する。
DMDLHyperlinkDetectorの初期処理(init()呼び出し)は不要になるので削除する。
DMDLHyperlinkDetectorでgetAdapter()からエディターを取得できるようにする必要があるので、その処理を追加する。
修正前 | → | 修正後 | 修正内容 |
---|---|---|---|
@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;
}
〜
}
|
自分自身を返す処理を追加。 |