ここでは、 Bulletnoteを開発した際に分かったことを中心に、 Eclipseのプラグインを 開発する際のFAQを勝手に設定して勝手に答えている。
Eclipseは、デザインパターンを活用した、 ポストモダンでデカダンでレーゾン・デートル なアーキテクチャになっているので、 そのプラグインを開発する際、非常に簡単なことを実現するにも 非常な困難にぶちあたることがままある。 いま目の前にあるテキストを取り出すにも、 耳の穴から指を突っ込んで奥歯をがたがたいわせながら舌をひっぱるような、 手の込んだメソッド呼び出しが必要になったりする。 パズルとして考えれば楽しくないこともなくもなくもないが、 プログラミングでいちいちこんなことをやらされるとなかなかつらいものがある。
加えて、ドキュメントが圧倒的に不足している。 一応Helpおよびいくつかの記事があるが、基本を網羅しているとは 言いがたいものがある。日本語の情報となると、壊滅的である。 「オープンソースなのだから、ソースを読め」とスパーハカー連中は いうかもしれないが、膨大なEclipseソースを読み解くのは困難極まりない。
なので、ここでは、プラグインを開発する際に判明したことを、 自分向けのメモとして記録し、少しでもドキュメントを 増やす努力をすることにしたい。 もちろん内容は無保証なことこの上ない。
Eclipseプラグインの開発はEclipse上で行う。 Eclipseを起動し、[File]->[New]->[Project]のあと、 [Plug-in Development]->[Plug-in Project]を選択せよ。 XMLエディタを必要とするプラグインを作る場合は、 Plug-in Code Generatorsのウィザードにおいて[Plug-in with an editor]を 選択すれば、簡単なXMLエディタが組み込まれた状態で プロジェクトが生成される。
基本的にJavaのパッケージ命名規則と同じにするのが作法のようだ。 ただ、困ったことに、IDにアンダーバーを含むことができない。 Package BOFで提供されているパッケージ名を使う場合は、 'jp.gr.javaconf...'とでもして、アンダーバーを除くしかないだろう。
[File]->[Import]->[Existing Project into Workspace]である。 分かりにくい。
正しい方法はよく分からない。が、プロジェクトのディレクトリ内にある、 '.classpath'の'classpathentry kind="src"'あたり、および 'build.properties'の'source.[プロジェクト名].jar = 'あたりを 書き換えると、まあまあうまくいく。
Your First Plug-inでも読んでがんばること。 基本的には、このplugin.xmlにextensionと呼ばれる拡張のエントリを記述し、 対応するクラスを記述していくことになる。 以下に、代表的なextensionの記述方法を挙げる。
org.eclipse.ui.editorsエクステンションを使う。 これによって、ある特定の拡張子を持つファイルを開いたときに 開くエディタを指定できる。たとえば、plugin.xml内に、
<extension point="org.eclipse.ui.editors"> <editor name="BulletML Editor" icon="icons/bulletml.gif" extensions="xml" contributorClass="jp.gr.java_conf.abagames.bulletnote.editors.BulletMLEditorActionContributor" class="jp.gr.java_conf.abagames.bulletnote.editors.BulletMLEditor" id="jp.gr.javaconf.abagames.bulletnote.editors.BulletMLEditor"> </editor> </extension>
のように記述すると、xmlファイルを開いたときに、 BulletMLEditorクラスで記述されたエディタが開くようになる。
IDE内の、エディタやツリー型のナビゲータ、エラーログやコンソールなど、 さまざまなウィンドウが配置される場をパースペクティブと呼ぶ。 org.eclipse.ui.perspectivesエクステンションを用いることで、 パースペクティブをカスタマイズできる。
<extension point="org.eclipse.ui.perspectives"> <perspective name="Bulletnote" icon="icons/bulletml.gif" class="jp.gr.java_conf.abagames.bulletnote.BulletnotePerspective" id="jp.gr.javaconf.abagames.bulletnote.BullenotePerspective"> </perspective> </extension>
パースペクティブの設定方法は、BulletnotePerspective内で、 IPerspectiveFactory.createInitialLayoutメソッドを実装することで行う。 詳細は、 Using Perspectives in the Eclipse UI参照。
既存の部品の拡張では不十分な場合、 org.eclipse.ui.viewsエクステンションを使うことができる。 このエクステンションは、ビューと呼ばれるものを定義することができる。 ビューは、さまざまな情報を表示できるウィンドウである。
<extension point="org.eclipse.ui.views"> <view name="Previewer" icon="icons/bulletml.gif" class="jp.gr.java_conf.abagames.bulletnote.Previewer" id="jp.gr.javaconf.abagames.bulletnote.Previewer"> </view> </extension>
ビューの作り方については、 Creating an Eclipse View参照。
org.eclipse.ui.editors.text.TextEditorを拡張すればよい。 テキストに色づけするなどのカスタマイズは、 setSourceViewerConfigurationメソッドで設定されるコンフィグレーション、 setDocumentProviderメソッドで設定されるドキュメントプロバイダで 行われているようだ。Plug-in Code Generatorsの生成する XMLエディタなどを参考にすること。
TextEditorを拡張したクラス内で、
String text = getDocumentProvider().getDocument(getEditorInput()).get();
とせよ。長い。
IFile openedFile = ((IFileEditorInput)getEditorInput()).getFile();
でIFileインタフェースが取得できる。
ちなみに、プロジェクト内の他のファイルを取得するときは、
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); IFile file = workspaceRoot.getFileForLocation(new Path(fileName).makeAbsolute());
エディタのエクステンションで指定した、contributorClassで感知できる。 org.eclipse.ui.texteditor.BasicTextEditorActionContributorを拡張した クラスの、setActiveEditorメソッドでフックせよ。
findViewメソッドを用いることで、ビューのIDから IViewPartのリファレンスをつかむことができる。 例えば、エディタのメソッドで、
IViewPart previewerPart = getSite().getWorkbenchWindow().getActivePage(). findView("jp.gr.javaconf.abagames.bulletnote.Previewer");
とする。
これを実現する方法はよくわからない。だが少なくとも、IViewPartインタフェースは、 IAdaptableインタフェースを継承しているので、getAdapterメソッドを用いて 無理やりリファレンスを渡すことはできる。 上の例を使うと、Previewerクラス内で、
public Object getAdapter(Class cls) { if ( cls.isInstance(this) ) { return this; } else { return super.getAdapter(cls); } }
としておき、
Previewer previewer = (Previewer)previewerPart.getAdapter(Previewer.class);
とすることでPreviewerのリファレンスをつかめる。 デザパタ的に正しい方法かどうかは分からない。
TaskListにタスクを追加するメソッドがTaskListクラスにあるとなどいう短絡思考をしている限り、 プラグイン開発などとてもおぼつかない。Eclipseフレームワークを流れるイベントやメッセージを 心の目で見、ある事象を引き起こすにはどこがその発火点なのかを肌で感じられるように 精進し、ビッグブルー様に拝礼を繰り返すことが、プラグイン開発の第一歩である。
タスクの追加は、エディタ上で編集しているリソースにマークすることで行う。 リソースにマークを行うことで、マークされた内容が自動的にタスクリストに追加される。 具体的には、エディタのメソッドで、
IResource resource = ((IFileEditorInput)getEditorInput()).getFile();として、現在編集中のソースのリソースをつかむ。そしてIResource.createMarkerメソッドを 用いて、マークを行う。
IMarker marker = resource.createMarker("org.eclipse.core.resources.problemmarker"); marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR); marker.setAttribute(IMarker.MESSAGE, message); marker.setAttribute(IMarker.LINE_NUMBER, lineNumber);
マーカーには、重要度やメッセージ、 ソース上のラインナンバーなど、さまざまな属性を設定することができる。 マーカーの詳細については、 Mark My Wordsを参照。
ViewPart.createPartControl(Composite parent)メソッドをオーバライドすることによってできる。 parentがこのビューの親ウィンドウなので、parent.setLayoutメソッドでレイアウトを設定できる。 なお、EclipseではGUI部品はAWTでなく、SWTで構成されているので、SWTの基本を学ぶこと。 SWTサンプル集などが役に立つ。
コードアシスト機能については、さらにドキュメントが不足しているので、 なかなか実装が難しい。以下の情報を得る際に、XMLエディタプラグインである X-Menのソースコードを参考にした。
まず、エディタにコードアシストを行うことを教える必要がある。 SourceViewerConfigurationクラス (AbstractTextEditor.setSourceViewerConfigurationメソッドでエディタに セットされる、ソースビューアの設定を行うクラス) 内の、getContentAssistantメソッドをオーバーライドし、 ContentAssistantおよびContentAssistProcessorを指定する。
public IContentAssistant getContentAssistant(ISourceViewer viewer) { ContentAssistant assistant = new ContentAssistant(); BulletMLContentAssistProcessor processor = new BulletMLContentAssistProcessor(editor); assistant.setContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE); assistant.setContentAssistProcessor(processor, XMLPartitionScanner.XML_TAG); assistant.enableAutoActivation(true); assistant.enableAutoInsert(true); assistant.install(viewer); return assistant; }
BulletMLContentAssistantProcessorクラスは、IContentAssistProcessorインタフェースを 実装した、補完すべきコードを計算し、提示するクラスである。
IContentAssistProcessorインタフェースを実装したクラスは、 setContentAssistProcessorメソッドによって、ContentAssistantに登録される。 登録する際には、プロセッサおよびそのプロセッサが動作する対象のコンテントタイプを 指定する必要がある。
コンテントタイプとは、IDocumentPartitionerインタフェースを実装した クラスで設定されるドキュメント内の各パーティションに、付加される属性である。 bulletnoteで用いている標準的なXMLエディタでは、 パーティショナーとして、XMLPartitionScannerをスキャナーに用いた DefaultPartitionerを用い、XMLドキュメント内のタグおよび コメントを、パーティションとして判別している。
上の例では、BulletMLContentAssistProcessorが、XMLのタグのコンテンツおよび デフォルトのコンテンツ(タグでもコメントでもないコンテンツ)があるパーティション上で コードアシストを実行した場合を受け持つプロセッサとして指定されている。
このようにして設定されたContentAssistantを、installメソッドを用いてソースビューアに 設定し、メソッドの返値として返す。 これでとりあえずコードアシストを行う実体(BulletMLContentAssistantProcessor)を エディタに教えることができた。長かった。すでに力尽きそうなので、ちょっと一息。
コードアシストを呼び出すための、独自のアクセラレータキーを設定することができる。 TextEditorに最初から設定されているコードアシストのアクセラレータキー (デフォルトではCtrl+Spaceに割り当てられている)をそのまま利用するのであれば、 以下の設定は必要ない。 次の「アクションをコードアシストに結びつけるには」まで読み飛ばすこと。
アクセラレータキーは、org.eclipse.ui.actionDefinitionsエクステンションおよび org.eclipse.ui.acceleratorSetsエクステンションによって 実現される。plugin.xmlに、以下のような記述を行う。
<extension point="org.eclipse.ui.actionDefinitions"> <actionDefinition id="jp.gr.javaconf.abagames.bulletnote.content.assist.proposals"/> </extension> <extension point="org.eclipse.ui.acceleratorSets"> <acceleratorSet scopeId="org.eclipse.ui.globalScope" configurationId="org.eclipse.ui.defaultAcceleratorConfiguration"> <accelerator key="CTRL+3" id="jp.gr.javaconf.abagames.bulletnote.content.assist.proposals"/> </acceleratorSet> <acceleratorSet scopeId="org.eclipse.ui.globalScope" configurationId="org.eclipse.ui.emacsAcceleratorConfiguration"> <accelerator key="CTRL+3" id="jp.gr.javaconf.abagames.bulletnote.content.assist.proposals"/> </acceleratorSet> </extension>
actionDefinitionsエクステンションは、その名のとおりアクションを設定する。 アクションとは、アクセラレータなどからビューやエディタに伝わる動作を表している。 plugin.xml内では、適当なIDさえ設定してやればよい。
acceleratorSetsエクステンションは、先ほどのアクションを、特定のキーと結びつける。 scopeIdおよびconfigurationIdによって、そのアクセラレータがどのエディタ上で有効か、 またどのコンフィグレーション([Preferences]->[Workbench]->[Key Bindings]で設定できる)で 有効かを設定することができる。
アクセラレータキーおよびそのキーが発生するアクションは、acceleratorエレメント内で 設定する。この例では、Ctrlを押しながら3を押すアクセラレータを、 上で定義したjp.gr.javaconf.abagames.bulletnote.content.assist.proposalsアクションに 結び付けている。
これでエディタにアクションが伝わるようになった。次はこのアクションを、 エディタのコードアシストに結びつける必要がある。 これは、AbstractTextEditor.createActionsメソッドをオーバーライドすることに よって行われる。
protected void createActions() { super.createActions(); IAction action = new TextOperationAction( ResourceBundle.getBundle("jp.gr.java_conf.abagames.bulletnote.editors.bulletnote"), "ContentAssistProposal.", this, ISourceViewer.CONTENTASSIST_PROPOSALS); action.setActionDefinitionId("jp.gr.javaconf.abagames.bulletnote.content.assist.proposals"); setAction("ContentAssistProposal", action); markAsStateDependentAction("ContentAssistProposal", true); }
コードアシストは、TextOperationActionの一種として扱われる。 TextOperationActionを、オペレーションコード(コンストラクタの4番目の引数)に ISourceViewer.CONTENTASSIST_PROPOSALSを設定して生成し、 setActionDefinitionIdメソッドを使い、先ほど設定したアクションのIDを設定する。 その後、setActionメソッドを用いて、このアクションをエディタに登録する。
TextEditorに最初から設定されているコードアシストのアクセラレータキーを利用する場合は、 アクションのIDとして、ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALSを 設定すればよい。
TextOperationActionは、コンストラクタの第1引数にResourceBundleを要求する。 ResourceBundleとは、言語に依存するテキストを扱うために用いられるものである。 詳細は、 Eclipseプラグインの国際化対応に詳しい。
TextOperationActionは、ResourceBundleをアクションの可視化のために利用すると 書いてある。が、これがなにを意味しているのかがよく分かっていない。 おそらく、アクションバーなどにこのアクションをバインドした際に 利用されると推測される。 とりあえず、X-Menのコードを見た限り、以下のようなファイルを作り、 'src/jp/gr/java_conf/abagames/bulletnote/editors/'ディレクトリに 'bulletnote.properties'という名前で保存しておけば良いようだ。
ContentAssistProposal.label=Co&ntent Assist@CTRL+3 ContentAssistProposal.tooltip=Content Assist ContentAssistProposal.description=Content Assist
そうなのである。ここまでやって、やっとコード補完を行う本体に取り掛かれるのだ。
前に述べたように、コード補完を行う部分は、IContentAssistProcessorインタフェースを 実装したクラスで行う。上記の設定を一通りやったあとならば、 コードアシストのアクセラレータを押すことで、 IContentAssistProcessor.computeCompletionProposalsメソッドが呼び出される。 このメソッドは、以下のような返値および引数を持つ。
ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int documentOffset)
ソースビューアそのものおよびコードアシストが呼び出されたカーソルの位置が渡されるので、 補完すべきコードの候補を返値として返せばよい。 各候補の内容は、 CompletionProposalクラスのコンストラクタなどを参照。
候補を作成する方法についてはここでは述べない。 というか、それこそものによって千差万別なので一概に述べることは無理である。 根性でソースの構造を解析し、カーソル位置に挿入する適切な文字列を作り上げること。
一例としてbulletnoteの補完候補作成方法を挙げると、
となる。情けなくなるほど簡単な方法だが、こんなものでもかなり役に立つ。
また、特定のキャラクタが入力されたときに、アクセラレータを押さずとも 自動的にコードアシストが立ち上がるようにもできる。 getCompletionProposalAutoActivationCharactersメソッドを実装せよ。
org.eclipse.ui.newWizardsエクステンションを用いることで、 ファイルやプロジェクトなどを新規生成するウィザードを作成することができる。
<extension point="org.eclipse.ui.newWizards"> <category name="BulletML" id="jp.gr.javaconf.abagames.bulletnote.bulletml.new"> </category> <wizard name="BulletML File" icon="icons/bulletml.gif" category="jp.gr.javaconf.abagames.bulletnote.bulletml.new" class="jp.gr.java_conf.abagames.bulletnote.editors.BulletMLCreationWizard" id="jp.gr.javaconf.abagames.bulletnote.wizards.new.bulletml"> <description> Create a BulletML file </description> <selection class="org.eclipse.core.resources.IResource"> </selection> </wizard> </extension>
categoryエレメントの属性には、Newダイアログの左側のツリーに表示される、 カテゴリが指定される。
wizardエレメントの属性には、ウィザードの名前、アイコン、所属するカテゴリ、 org.eclipse.ui.INewWizardインタフェースを実装したクラス、IDなどを指定する。
INewWizardインタフェースを実装したクラスでは、ウィザードへのページの追加、 'Finish'ボタンが押されたときの動作などを規定する。 このクラスを作成するには、org.eclipse.ui.internal.dialogs.NewWizardをextendsした クラスを作成し、いくつかのメソッドをオーバーライドするのがお手軽と思われる。 initメソッドでタイトルの設定およびIStructuredSelectionの取得、addPagesメソッドで ファイル生成のためのページの追加、performFinishメソッドで 前記ページで設定したファイルの実際の生成を行うとよい。
org.eclipse.ui.dialogs.WizardNewFileCreationPageをextendsするのが簡単である。 getInitialContentsメソッドで、ファイルに与える初期テキストを設定することができる。
また、createFileメソッドを以下のようにすることで、 生成されたファイルをエディタで自動的に開くことができる。 この情報については 安藤さんに 教えていただいた。
public void createFile() { IFile file = createNewFile(); IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); try { page.openEditor(file); } catch (PartInitException e) {} }
本家の提供する Eclipse technical articles、 および付属のHelpはとても役に立つ。
日本語の情報源は、 Eclipse プラグインの開発などがある。 サンプルソースという意味では、 Eclipse用2ちゃんブラウザ monalipseなども。 また、Emacsライクな入力方式を実現するプラグイン EE2Eのページにも 有用な情報、リンクがある。