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;
}
〜
}
|
自分自身を返す処理を追加。 |