本章では、Eclipseのエディタの構造、およびその変更方法につい て記述する。 まず、事前準備項でソース閲覧のための準備について述べ る。Eclipse のテキスト編集機能項で、Eclipseのワークベ ンチが提供する汎用エディタとその基本機能および実装について説明する。 そして、JDT の Java エディタ項で Java 専用エディタの実装につい て、ワークベンチのエディタ機能との関連を交えて説明する。 最後に、 Java エディタの拡張項で、Java エディタへの機能追加の実 装事例を紹介するとともに、pxml 対応 Java エディタを実装する上での課題に も触れる。
本ドキュメントを理解するうえで、Eclipse のソースおよび、これを効率よく 閲覧するためのツールが必要不可欠である。 そこで、本ドキュメントを読み進めていく上での事前準備として、本節では Eclipse のソースツリーを Eclipse で閲覧する方法について説明する。 なお、本節の内容には前回の調査報告の内容と重複する。手順等の詳細について は前回の調査報告を参照いただきたい。
本項では、Eclipse のソースツリーを Eclipse のワークスペースにイ ンポートする方法を説明する。 インポート作業は下記の手順で行う。
Eclipseのソースを展開する。展開先(pluginsディレクトリを含むディレクト リ)を${ECLIPSESRC}とする。
Eclipseを起動し、ファイル (File)->新規 (New)->プロジェクト (Project) を選択。
新規プロジェクト (new project) が表示されたら、ダイアログの左側でシンプ ル(Simple)を選択し、右側でプロジェクト(Project) を選択する。Next をクリッ ク。
プロジェクト指定ダイアログで、デフォルトの使用 (Use default) のチェック をはずし、${ECLIPSESRC}/plugins/<プラグイン名> をDirectoryに指定す る。プロジェクト名 (Project name) には、<プラグイン名>を指定する。 Finish をクリック。
以上の作業を、必要となるプラグイン全てに対して行う。 依存関係のあるプラグインが取り込まれていないととエラーが表示されるが、こ れは必要なプラグインを全て取り込むことで表示されなくなる。
取り込み後、「Java ブラウズ」パースペクティブ等を利用することで、効率的 にソースが閲覧できる。
Eclipse のソースはプラグインが一つのプロジェクトになるため,ワークスペー スが Eclipse プラグインでいっぱいになって、その他のプロジェクトの参照 性が下がってしまう。 こういうときには、複数のプロジェクトをまとめて1つの 作業セット (working set) に登録すると効果的である。 使用方法は「リソース」パースペクティブの「ナビゲータ」ビューや、「Java」 パースペクティブの「パッケージ・エクスプローラー」ビュー、「Javaブラウズ」 パースペクティブの「ナビゲータ」ビューなどの右上にある▼メニューをクリッ クし、「作業セットの選択(select working set...)」などを選択する。 |
本ドキュメントを読み進める上で必要となるプラグインは 図1のとおりである。
org.eclipse.swt プラグインを取り込むと、.classpath がないというエラーが 表示される。 org.eclipse.swt のプラグインディレクトリ直下にある .classpath_<ウィ ンドウシステム名> の中のいずれかを .classpath にコピーすることで、こ のエラーは表示されなくなる。
|
なお、図1には依存関係があるパッケージや、将来 的にエディタ拡張の過程で必要となることが想定されるプラグインなども一部含 まれている。従って、本ドキュメント中に直接登場するプラグインはこのうちの ごく一部である。
本節では Eclipse のワークベンチが提供するテキストエディタ関連のフレー ムワークについて説明する。
まず最初に、Eclipse でのエディタがどういう構成になっているかについて簡 単に説明する。
Eclipse のエディタのユーザインタフェイスや動作などに関する基本機能は、 ワークベンチでフレームワークとして提供されている。 また、ワークベンチはそれ自身が基本的なテキストエディタも提供しており、 ワークベンチが提供するこのテキストエディタはデフォルトエディタと呼ばれて いる。 デフォルトエディタはワークベンチのエディタフレームワークの、最もシンプル な利用例と捉えることができる。
ワークベンチとは、Eclipse のコア部分のことである。Eclipse は言語や用 途に関わらず必要となる一般的な機能をワークベンチとして提供している。一方 で、Java 開発環境など特定の言語や環境に依存した機能は、ワークベンチとは 独立したプラグインとして実装し、 Eclipse ワークベンチに個々のプラグイ ンを追加することで、環境依存の機能が利用できるようになる。Eclipse の配 布パッケージには、ワークベンチのほかに JDT (Java Development Toolkit) プラグインと、PDE (Plugin Development Environment) プラグインが同梱されている。 なお、実際にはワークベンチ自体もプラグインと同様の構成を取っているため、 下記ではワークベンチプラグインといった表記も登場する。 ワークベンチや JDT, PDE は何れも機能が多岐にわたるため、実際には複数のプ ラグインの集合として提供されている。 |
一方で、JDT が提供する Java エディタなど、特定の書式にカスタマイズされ た編集補助機能を保有するエディタは、個別のプラグインで提供される。 これらの、これらのカスタマイズされたエディタも、デフォルトエディタと同様 にワークベンチが提供するエディタフレームワークを利用することとなるが、 その際に、対象としているドキュメントの書式に依存した様々な機能を付加的に 実装して付け加える必要がある。
デフォルトエディタや Java エディタと、各種エディタフレームワークとの関係 を整理した図が 図2である。 ここまでは Eclipse のエディタフレームワークとひとくくりにしてきたが、 実際にはフレームワークは3種類のサブフレームワークから構成されている。 それぞれ SWT が提供する基礎的なユーザインタフェイスフレームワーク、 JFace が提供するテキストエディタのユーザインタフェイスに 特化したフレームワーク、 さらにワークベンチが提供する包括的なテキストエディタフレームワークである。
Eclipse はユーザインタフェイス用フレームワークとして Standard Widget Toolkit (SWT) と JFace の2種類を提供している。 SWT は下位レベルのユーザインタフェイスツールキットである。SWT では OS や ウィンドウシステムに依存しない API を提供する。といっても、ウィンドウシ ステムが提供するウィジェットを隠蔽し、その他の部分の実装のシステムへの非 依存性を保証するのが主要な機能なので、基本的には諸々のウィンドウシステム が提供すると同等のレベルの API と考えてよい。 一方で、JFace は SWT よりも上位のレベルの機能を提供するユーザインタフェ イスツールキットである。具体的には、ダイアログやウィザード、アクションな どといった機能が提供される。JFace の殆んどの機能は SWT をベースとして実 装されており、JFace フレームワークに則ったユーザインタフェイスを作成する 限りにおいては SWT を殆んど参照することなくユーザインタフェイスを構築す ることが可能である。しかし、JFace は SWT を完全に隠蔽するものではないの で、必要に応じて SWT を直接利用することも可能である。 なお、JFace は J で始まるが Java エディタ専用というわけではなく、 Eclipse の汎用のフレームワークである。 |
これらの3種類のテキストエディタフレームワークのうち、SWT と JFace に関 するものは、ユーザインタフェイス、つまり入力および表示に関する機能が集約 されている。一方でワークベンチのテキストエディタフレームワークは、表示の みならず、テキストエディタの動作そのものに関する機能を提供している。 また、表示に関する諸々のフレームワークを集約し、より実装がしやすいように 整理されている。
なお、上記の図ではそれぞれのコンポーネントから SWT への線が引かれている が、実際は JFace のエディタのフレームワークが SWT フレームワークのかなり の部分を隠蔽しており、エディタ機能の実装を行う過程で SWT のクラス等を直 接参照することはまずないといっていい。 実際、今回の調査報告でも SWT に属するクラスは一切登場しない。
次項以降では、まず Eclipse ワークベンチのデフォルトエディタについて説 明した上で、ワークベンチおよび JFace が提供するエディタフレームワークを 説明する。
デフォルトエディタは Eclipse でファイル等を開く際に、そのファイルタイ プに対応する拡張エディタが規定されていなかった場合に、使用されるエディタ である。 Eclipse で .txt などのファイルを開いたときに、内容の表示に使われている エディタがデフォルトエディタであると考えて間違いない。 デフォルトエディタは、本文の表示や入力された文字の挿入はもとより、 範囲選択やカット&ペーストなど、テキストエディタが持っている基本的な編集 機能を全て提供している。
デフォルトエディタの実体はワークベンチプラグイン org.eclipse.ui.editors の org.eclipse.ui.editors.text パッケージにある TextEditor.java である。 このソースは org.eclipse.ui.editors プラグイン の src/org/eclipse/ui/editors/text/TextEditor.java にある。
Eclipse で複数のプロジェクトの中から特定のクラスやメソッドを検索する際 には、「検索(Search)」メニューの「検索(Search)」を選択し、開いたダイ アログの「Java 検索(Java Search)」を活用するとよい。 また、既に表示されているソースコードの参照クラス等を見たい場合は、ソース 中のクラスやメソッドを選択し、右ボタンメニューから「宣言を開く(Open Declaration)」や「検索(Search)」→「参照(References) / 宣言(Declarations) / インプリメンター(Implementors)」→「ワークスペース(Workspace)」などを 活用するとよい。各種ビューでも右ボタンメニューから同様の検索が可能である。 |
この TextEditor クラスは、Eclipse のワークベンチが提供するテキストエディ タ用のフレームワークを殆んどそのまま用いて、ミニマムのテキスト編集機能を 持つエディタとして実装したものである。 従って、実装の殆んどは、ワークベンチのテキストエディタ用フレームワークの クラスに依存している。 デフォルトエディタクラス TextEditor のクラス階層を 図3 に示す。
図中の TextEditor がデフォルトエディタである。また、JavaEditor は Java の編集のために様々な機能を付加したエディタである。 なお、JavaEditor については、JDT の Java エディタ項で解説する。
ワークベンチプラグインをはじめとして殆んど全てのプラグインは内部に複数の Java パッケージを保有している。例えば、ワークベンチプラグイン org.eclipse.ui.editors は下記のパッケージを含んでいる。
プラグイン内のパッケージがプラグイン名のサブパッケージになっているとは限 らないので注意が必要である。例えば、org.eclipse.ui.editors パッケージに は上記の様に org.eclipse.ui.texteditor パッケージが含まれている。 |
この図から、Eclipse のデフォルトエディタや Java エディタは共通のクラス StatusTextEditor がベースとなっていることがわかる。 この StatusTextEditor クラスはワークベンチプラグイン org.eclipse.ui.workbench.text の org.eclipse.ui.texteditor パッケージに 属するクラスである。 実際には StatusTextEditor は同じく org.eclipse.ui.texteditor パッケージに 属する AbstractTextEditor クラスを継承しており、この AbstractTextEditor がテキストエディタの基本機能を提供している。
TextEditor は org.eclipse.ui.editors プラグインのクラス。一方で StatusTextEditor 等は org.eclipse.ui.workbench.texteditor プラグインであ る。 たちが悪いことに両方とも同じ org.eclipse.ui.texteditor というパッケージ を持っている。プラグインの混同に注意。 |
Eclipse のテキストエディタはテキストエディタフレームワークの中 のITextEditor インタフェイスを実装していなければならないが、ITextEditor で求められる機能は極めて多いため、Eclipse のエディタを実装するためには 事実上テキストエディタフレームワークをベースとして構築しなければならない といっていい。図4にあるよ うに、AbstractTextEditor が ITextEditor インタフェイスの実装クラスとなっているため、AbstractTextEditor や StatusTextEditor を継承したクラスを実装することによってこの条件も満たせ る。
逆にいうと、Eclipse ワークベンチが提供するテキストエディタ用のフレーム ワークは、デフォルトエディタに限らず、テキストエディタを実現する上で必要 となる様々な機能を提供している。従って、テキストエディタフレームワークを 活用することで様々な拡張エディタを比較的容易に実現することが可能となる。
デフォルトエディタでも、基本的なエディタの動作は全てワークベンチプラグイ ン org.eclipse.ui.workbench.texteditor のテキストエディタフレームワーク に依存している。 TextEditor.java ではファイルの読み書きやアクションの設 定、キーバインドをはじめとするエディタの動作の諸設定、行番号などを表示す るルーラーの実装を保持しているに過ぎない。これらについても殆んどは テキストエディタフレームワークの機能を利用しているだけで、TextEditor の 内部では設定の管理と細かな動作の記述がなされているだけである。 従って、TextEditor.java を見ても、フォントの装飾やポップアップなどの仕組 みがどうなっているかを図り知るのは不可能である。
以降では、まず、ITextEditor や AbstractTextEditor などを包含する org.eclipse.ui.workbench.texteditor プラグインを概観する。 その後で、フォントの装飾やポップアップと直接的に関係する JFace 関連のプ ラグインのテキストエディタフレームワークについて概観する。
先に述べたとおり、Eclipse のデフォルトエディタをはじめとするテキストエ ディタはワークベンチプラグイン等が提供する機能を利用して実現されている。 本項では、Eclipse が提供するテキストエディタフレームワークのうち、 org.eclipse.ui.workbench.texteditor プラグインの主要クラスについて、構成 や機能等を解説する。 特に、今回の調査のスコープとの関連性が高い動作設定とアクション定義につい て説明する。
Eclipse のテキストエディタフレームワークは、ワークベンチのユーザインタ フェイスフレームワーク JFace が提供するテキスト表示機能等を集約し、さら にエディタとしての基本機能を付加したものである。 Eclipse のテキストエディタフレームワークはワークベンチプラグイン org.eclipse.ui.workbench.texteditor で提供されている。 先に、Eclipse のエディタは ITextEditor インタフェイスを実装しなければ ならないと述べたが、この ITextEditor も org.eclipse.ui.workbench.texteditor プラグイン内の src/org/eclipse/ui/texteditor/ にある。また、デフォルトエディタ TextEditor クラスの親クラス StatusTextEditor も org.eclipse.ui.workbench.texteditor プラグインにあるクラスで、プラグイン のソースツリーの中の src/org/eclipse/ui/texteditor/StatusTextEditor.java にある。
org.eclipse.ui.workbench.texteditor プラグインの org.eclipse.ui.texteditor パッケージは、テキストエディタそのものの基底ク ラス群のほか、テキストエディタにまつわるインタフェイス、テキストエディタ の動作(アクション)の実装クラス・インタフェイス群、およびこれらが使用す るクラスから構成されている。
先に述べたとおり、テキストエディタに提供する機能の大部分は AbstractTextEditor クラスに集約されている。 AbstractTextEditor では、テキストエディタを構成する主要な機能を実装した クラスのインスタンスを保持している。 本項では、この AbstractTextEditor が管理するインスタンスのうち、下記につ いて概観する。
fSourceVeiwer
fSourceViewerConfiguration
fInternalDocumentProvider
fPreferenceStore
fActions
fSourceViewer は ISourceViewer インタフェイスの実装クラスのインスタンス である。 AbstractTextEditor の fSourceViewer は、テキストエディタのインスタンスが 編集対象としているドキュメントの表示を担当する。 ISourceViewer の実装クラスはソースコードの表示に関わる機能を提供するクラ スで、org.eclipse.jface.text プラグインの org.eclipse.jface.text.source パッケージで提供されている。
ISourceViewer はテキストファイルの表示機能を提供する ITextViewer を継承したインタフェイスであり、通常のテキスト表示機能に、プログラムソー スの表示で必要となる機能を追加したものである。 ITextViewer は org.eclipse.jface.text プラグインの org.eclipse.jface.text パッケージに含まれている。 ISourceViewer や ITextViewer は表示機能のみを管轄しており、文字入力を はじめとする編集機能は直接的には保有していない。 ちなみに、ITextViewer は、表示の形式等を管理・決定するクラス TextPresentation を内部で管理している。
ITextViewer の実装クラスとして TextViewer クラスが、また ISourceViewer の実装クラスとして SourceViewer がそれぞれインタフェイスと同じパッケージ 内に提供されている。 クラスの継承関係等は図5および 図6を参照。 Java ソースのビューアクラス JavaSourceViewer も SourceViewer を継承した クラスとして実装されている。
fSourceViewerConfiguration は ISourceViewer の実装クラスの設定を管理する クラスである。 SourceViewerConfiguration は org.eclipse.jface.text プラグインの org.eclipse.jface.text.source パッケージで提供されている。 このクラスは JFace フレームワークが提供する様々な拡張機能 の設定を一括して管理しているクラスである。 SourceViewerConfiguration を ISourceViewer の configure メソッドで渡して やることで、SourceViewer 自身の表層を定義している。 なお、JFace が提供する諸機能については、JFace フレームワークが提供するエディタの機能項 を参照のこと。
fInternalDocumentProvider は、IDocumentProvider インタフェイスのイン スタンスで、TextEditor と編集対象のドキュメントの実体(ファイルなど)と の対応付けを行っている。 IDocumentProvider は org.eclipse.ui.workbench.texteditor プラグインの org.eclipse.ui.texteditor パッケージで提供されている。 諸々のパッケージでファイルなどを対象とした実装クラスが提供されている (図7)。
fPreferenceStore は IPreferenceStore インタフェイスのインスタンスで、 エディタに関する様々な設定を保持している。 IPreferenceStore は一種のハッシュテーブルで、機能的には java.util.Properties をリッチにしたものと考えてよい。実体は org.eclipse.jface プラグインの org.eclise.jface.preference パッケージに ある。 AbstractEditor では、エディタの設定を保持するためにこの IPreferenceStore クラスを使用している。 ここで対象となる設定項目とは、例えばエディタの表示色指定やキーバインドな どである。 fPreferenceStore が保持する内容はエディタの諸機能の実装部分等から参照で きるため、エディタの拡張機能を実装した際に実装した機能に関連する設定を新 たに fPreferenceStore に持たせることも可能である。 ただし、設定項目を追加した場合、新たに追加された設定項目を変更するユーザ インタフェイスも必要となる。
fActions は AbstractEditor が管理するアクションの一覧を保持する HashMap である。 アクションとは、キー操作などに連動して行うエディタの動作を定義したもので、 カットアンドペーストや、検索、置換などがその代表例である。 AbstractTextEditor が管理するアクションについては テキストエディタのアクション定義項を参照。
テキストエディタフレームワークでは、カットアンドペースト、検索、置換など、 テキストエディタに関連する様々な基本編集作を用意している。これらの動作は、 org.eclipse.ui.workbench.texteditor プラグインの org.eclipse.ui.texteditor パッケージのクラスや、AbstractTextEditor の内 部クラスなどとして提供されている。
表1はその一例である。
表 1. テキストエディタフレームワークが提供するアクションの例
アクション | 実装クラス |
---|---|
行単位の削除 | DeleteLineAction |
文字列の検索 | FindNextAction |
文字列の置換 | FindReplaceAction |
カーソルを指定された行に移動する | GotoLineAction |
インクリメンタルサーチ | IncrementalFindAction |
図8 にテキストエディタに関するアクション クラスの一覧と継承関係を示す。
この中の ResourceAction 以下のクラスはいずれも org.eclipse.ui.texteditor パッケージのクラスである。 org.eclipse.ui.workbench.texteditor プラグインで提供されるこれらのアクショ ンは、何れも TextEditroAction やその親クラスの ResourceAction などを継承 していることがわかる。 Eclipse フレームワークではキー入力などをハンドルする Action は IAction インタフェイスを実装していなければならない。この階層の中では Action クラ スが IAction を実装したクラスになっている。
これらのアクションは、AbstractTextEditor クラスの createActions メソッド で実体が作成され、エディタのアクションとして関連付けが行われる。
なお、カット、コピー、ペーストなどの編集機能についてはコアプラグイン org.eclipse.ui.workbench で提供されている。これらは、 Eclipse UI/org/eclipse/ui/actions/ ディレクトリの中の TextActionHandler の内部クラスとして定義されている。
dabbrev を実装する場合、上記のアクションと群と同様に ResourceAction もし くは TextEditorAction を継承したクラスを作成し、その中に動作を記述す ることになる。 dabbrev では、通常近隣の行を検索し入力された文字列にマッチする行を検索す ることとなる。従って、FindNextActoin や DeleteLineAction などは参考にな る可能性がある。 また、dabbrev の操作ではインクリメンタルサーチと同様に2 回以上連続してアクションが呼ばれたときに動作が変化する(順位の低い候補を 表示する)ので、IncrementalFindAction クラスや、同クラスを補助している IncrementalFindTarget クラスなどが参考になるものと思われる。 |
本来であれば、このほかの部分についても詳細な報告をすべきかもしれないが、 今回調査対象になっている部分の殆んどは org.eclipse.ui.workbench.texteditor プラグインではなく、JFace フレームワー ク関連のプラグインで提供されている。 そこで、org.eclipse.ui.workbench.texteditor のフレームワーク解説はここで 一旦終了し、次節では JFace の提供するテキストエディタフレームワークにつ いて説明する。
通常の拡張エディタは、Eclipse のワークベンチが提供するエディタフレーム ワークを利用して、対象となるドキュメントの種類に応じた特殊なアクションや 編集機能、表示機能などを追加することで実現される。 前節では、テキストエディタフレームワークを用いて実現できる拡張機能として アクションの追加について簡単に述べた。 しかしながら、今回の調査範囲の機能のうち、特定言語に対応した色付けや、テ キストの自動インデント、コードアシストなどの機能に対する拡張は、 テキストエディタフレームワークより下位の JFace フレームワークによって提 供されている。デフォルトエディタや Java エディタも JFace フレームワーク を利用して色づけなどを行っている。 そこで、本節では、JFace の中のテキストエディタに関するフレームワークが提 供する拡張方法について説明する。
先にも述べたが JFace は Eclipse に対してユーザインタフェイス機能を提供 する高位のフレームワークであり、ワークベンチプラグインの一部として提供さ れている。JFace フレームワークの大半の機能はワークベンチプラグイン org.eclipse.jface で提供されているが、テキストエディタに関連したフレーム ワークはワークベンチプラグイン org.eclipse.jface.text で提供されている。
エディタの諸機能のうち JFace のテキストエディタフレームワークが提供して いる機能は下記のとおりである。
フォントの装飾やスタイルの変更
テキスト中に現れる特定の文字や範囲に対して、指定された条件に従って文字色 やフォント、フォントのスタイルを変更する機能を提供する。
テキストの整形(フォーマット)
文脈に応じて、テキスト中に空白や改行を適宜挿入しソースコードを見やすくす る機能を提供する。
補完候補の出力
文脈に応じて補完候補を表示、選択する機能を提供する。
ポップアップによるヘルプ等の表示
マウスカーソルの位置に応じて適切なコメントやヘルプをポップアップウィンド ウに表示する機能である。Java エディタの場合は、クラスやメソッド等の JavaDoc コメントや、変数の型、カーソル位置に関するエラーや警告のポップアッ プ表示に利用されている。
外部のドキュメント情報との連携
外部のドキュメント情報との連携は、エディタ以外の部分が保持しているドキュ メントのテキストに関連した情報との連携を行う。 例えば、Java ソースファイルの場合、外部でソースの構文木情報などのドキュ メントモデルデータを管理しているが、エディタで Java ソースを編集した際に は、変更内容に応じて構文木情報を修正しなければならない。 この機能は、このようなテキストの編集に伴って発生するテキストエディタ外 のドキュメント情報の修正のトリガとして利用される機能である。
次項以降では、これら JFace のテキストフレームワークが提供する諸機能の仕 組みと活用方法について概説する。
まず最初に、ここでは上記の諸機能に共通する部分として、拡張機能の設定方法 について概説する。
JFace テキストフレームワークが提供する機能の概要項で述べたように、jface のテキストフ レームワークでは多種多様な機能を提供している。 これらの機能は何れも対象となる位置の文脈など、ドキュメントにまつわる条件 ごとに、その条件下で行うべき動作を指定する必要がある。 フォントの装飾を例に取ると、ここでいう条件とは対象部分が文字列であるか予 約語であるか平文であるか、といったものであり、また動作とは文字列であれば 青色、コメントは茶色、本文は基本は黒だけど予約語だけは太字の紫にする、と いったものである。
従って、このような条件と動作の組み合わせを与えてやることで、これらの機能 を実現することが可能となる。 Eclipse の JFace テキストエディタフレームワークで提供する機能は、いず れもこの条件と動作のセットで定義される。
このうち条件は内容種別を表す String で付与する。例えば、文字列は "QUOTED_STRING", コメントは "COMMENT", それ以外は "OTHERS" という名前を 割り当てる。 一方で、動作はそれぞれの機能ごとに動作を記述したクラスを作成する。 JFace フレームワークでは、個別機能ごとにインタフェイスやテンプレートクラ スが用意されており、個別のエディタではこれらを継承したクラスを実装するこ とで動作を付与する。 内容種別の文字列と動作記述クラスとの対応関係は HashMap が保持しており、 内容種別を与えることで実際の動作の定義を取得することが可能となる。
このような構成になっているのは、編集テキストの部分ごとにフォント装飾や テキスト整形のストラテジ(動作)が大きく異なるためではないかと想像され る。 例えば Java のソースコードについて考えてみると、普通のソースコードの部分 とコメント部分ではフォント装飾やコードアシストのストラテジは全く違ったも のである。そこで、内容種別ごとに動作が定義できるようになっているのではな いかと思われる。例えば、Java エディタでは、下記の6種類のパーティション が用意されている。
一行コメント (// で始まるコメント)
複数行にわたるコメント(/* から */ まで)
JavaDoc コメント(/** から */ まで)
文字列定数(ダブルクウォートで囲まれた部分)
文字定数(シングルクウォートで囲まれた部分)
Java プログラム本文(上記以外)
なお、これらの内容種別や動作の役割と関係は、 JDT の Java エディタ項で説明する。
さて、実際には、内容種別に対応した動作記述クラスを取得する前に、対象とな るドキュメントを解析し、テキスト中の種別の境界を決定したり、個別の部 分ごとにドキュメントの内容種別を判別する必要がある。 内容種別とその境目の情報はパーティションと呼ばれており、これらのパーティ ションの判定についてもフレームワークが提供されている。 なお、パーティションの分割および内容種別の判定の詳細については テキストのパーティション項 で説明する。
エディタが提供する諸機能は、全て org.eclipse.jface.text.source パッケー ジの SourceViewerConfiguration クラスで一括管理されている。 拡張機能を実装した場合、実装した拡張機能のクラスを SourceViewerConfiguration のインスタンスに登録し、 このインスタンスを AbstractTextEditor の setSourceViewerConfiguration メ ソッドに渡してやることで、機能が利用できるようになる。
本項ではテキストのパーティションについて説明する。 パーティションとは、編集対象テキストを、内容などに応じて部分ごとに分割し た際の個々の断片のことである。 JFace テキストフレームワークでの拡張機能設定項で述べたように、Eclipse のエ ディタフレームワークでは、内容種別ごとにフォント装飾やテキスト整形の ストラテジを定義することが可能である。 テキストのパーティションの特徴は下記のとおりである。
パーティションは重複しない
テキストの全ての文字は必ずどれか1つのパーティションに属する
パーティションには必ず1つの内容種別が割り当てられる
JFace テキストフレームワークでは、このパーティションをスキャンするための 機能を提供している。このパーティションとスキャニングのフレームワークは ワークベンチプラグイン org.eclipse.jface.text の org.eclipse.jface.text.rules パッケージにある。 このパッケージではパーティションに関するインタフェイスのほかに、ルールベー スのスキャナのテンプレートなども提供している。 また、JFace プラグインが提供する装飾などの機能についても、テンプレートと なるクラスを提供している。 関連するクラスおよびインタフェイスを表2 に示す。
表 2. パーティションに関するクラス
名称 | 種別 | 役割 |
---|---|---|
IPartitionTokenScanner | インタフェイス | パーティションをスキャンするクラスのインタフェイス。ITokenScanner を継承 している。 |
ITokenScanner | インタフェイス | IPartitionTokenScanner の親インタフェイス。主要なメソッドは nextToken。 nextToken を実行すると、次のパーティション情報を返す。 パーティション情報は IToken インタフェイスの実装クラスに格納する。 ITokenScanner ではパーティション分割に限らず、一般的なスキャニングの用途 で使用できる。 |
RuleBasedPartitionScanner | クラス | IPartitionTokenScanner のテンプレート実装クラス。ルールに基づいたパーティ ションのスキャン機能を提供する。 ルールは IRule インタフェイスの実装クラスで定義する。 同じパッケージ内の BufferedRuleBasedScanner を継承。 |
RuleBasedScanner | クラス | RuleBasedPartitionScanner の親クラス(間に BufferedRuleBasedScanner があ るが)であり IPartitionScanner のテンプレート実装クラス。 ルールに基づく基本的なスキャン機能を提供する。 ルールは RuleBasedPartitionScanner と同様に IRule インタフェイスの実装ク ラスで定義する。 |
RuleBasedPartitioner | クラス | RuleBasedParitionScanner を利用してドキュメントのパーティション分割を行 うクラス。org.eclipse.text プラグインの org.eclipse.jface.text パッケー ジの IDocumentParitioner インタフェイスの実装。 |
IToken | インタフェイス | パーティションの内容種別を格納するクラスのインタフェイス。 パーティションの内容種別文字列は getData メソッド の戻り値を String にキャストすることで取得する。 |
Token | クラス | IToken の実装クラス。 |
IRule, IPredicateRule | インタフェイス | RuleBasedParitionScanner で利用するパーティション分割規則を定義するクラ スのインタフェイス。 |
NumberRule, WhiteSpaceRule, WordRule, PatternRule, MultiLineRule, SingleLineRule, EndOfLineRule, WordPatternRule | クラス | IRule の実装クラス群。 |
基本的には IPartitionTokenScanner の実装クラスを用意すればよい。 実装クラスは setRange もしくは setPartialRange でスキャンする範囲を指定 でき、かつ nextToken メソッドで範囲内のパーティションを順次取得できる ように実装する必要がある。 nextToken の戻り値は IToken インタフェイスである。IToken は is 系のメソッ ドでトークンの種別が取得できるほか、getData メソッドの戻り値を String に キャストすることで内容種別を取得できるようにする必要がある。
IPartitionTokenScanner の実装クラスとして RuleBasedPartitionScanner が用 意されている。このクラスは IRule インタフェイスの実装クラスによって与え られるルールを組み合わせることで、テキストをスキャンする機能をより簡単に 実装できる。Java エディタでは、パーティションのスキャンに RuleBasedScanner を継承した JavaPartionScanner クラスを利用している (図9)。
ソースの解析やトークンというと、前回の調査報告で述べた Java コンパイラの ソース解析機能 (org.eclise.jdt.core プラグインの compilre ディレクトリに ある org.eclipse.jdt.internal.compiler.parser.Scanner クラスなど)や、そ の際に利用するトークン(org.eclipse.jdt.core プラグインの dom ディレクトリ以下にある org.eclipse.jdt.core.dom.ASTNode クラス や model ディレクトリ以下にある org.eclipse.jdt.core.IJavaElement など)との関係 が気になる。 しかし、残念ながら JFace テキストフレームワークが用意するスキャナやトー クンと、JDT のコンパイラにあるこれらのクラスとは一切無関係である。 スキャナについては、JDT では Java エディタ専用のトークンおよびスキャナを Java コンパイラとは完全に独立して実装しており、トークンの取得にはこのス キャナを利用している。 |
フォントの装飾に関する機能は org.eclipse.jface.text プラグインの org.eclipse.jface.text.presentation パッケージに用意されている。 presentation パッケージのクラスは下記の 4 つである。
表 3. フォントの装飾に関するクラス
名称 | 種別 | 役割 |
---|---|---|
IPresentationDamager | インタフェイス | テキストの装飾の修正範囲を特定する |
IPresentationRepairer | インタフェイス | テキストの装飾を修正する |
IPresentationReconciler | インタフェイス | 内容種別ごとの IPresentationDamager および IPresentationRepairer を保持 する |
PresentationReconciler | クラス | IPresentationReconciler のテンプレート実装 |
この機能は、文字入力などによって生じたテキストへの変更に対し、全体を再描 画するのではなく、影響が波及する範囲のみを再描画することを想定した実装に なっている。 例えば、1文字入力された場合、大抵はその文字、あるいはせいぜいそ の単語のみを変更すればよく、それ以外の部分については文字が入力される前の 表示がそのまま利用できる。 このように、テキスト全体を再描画する必要があるケースはそれほど多くはない。 1文字入力されるたびにテキストビューを全て再描画してしまうと極めて効率が 悪くなる恐れがあることから、 テキストが変更される都度、変更によって表示の修正が必要となる必要 最低限の範囲を特定し、その部分のみを修正することを意図しているものと思わ れる。 なお、最初にテキストを表示する時には、テキスト全体が変更されたものするこ とで全体を描画させることが可能である。
jface.text.presentation パッケージのうち、テキスト修正動作に直接関連する ものは、IPresentationDamager と IPresentationRepairer である。 IPresentationDamager インタフェ イスは、テキストの変更によって表示の修正が必要となる範囲を特定するクラ スのインタフェイスである。 IPresentationDamager が特定した修正範囲に対し、実際にテキストの修正を行 うクラスのインタフェイスが IPresentationRepairer である。
IPresentationDamager は getDamageRegion というメソッドを持っている。 このメソッドは、 テキストが編集などにより変更された際に、変更が行われた範囲と変更内容を受 けて、その変更が影響する範囲を返す。 IPresentationRepairer には createPresentation というメソッドがあり、この メソッドは、IPresentationDamager の getDamageRegion の返した範囲情報と、 その部分へのテキスト表示設定を受け取ると、テキスト表示設定の内容を更新す る。
一方で IPresentationReconciler は、テキストの内容種別に対して適切な IPresentationDamager および IPresentationRepairer を提供する、いわば HashMap 的クラスのインタフェイスである。jface.text.presentation パッ ケージでは、IPresentationReconciler のテンプレート実装として PresentationReconciler クラスを提供している。 大抵のエディタでは、 PresentationReconciler は拡張することなくそのまま利 用可能である。実際に、Java エディタでも PresentationReconciler がそのまま 利用されている。
IPresentationDamager と IPresentationRepairer については、 jface.text.presentation パッケージにはインタフェイスしかないが、 org.eclipse.jface.text.rules パッケージに実装クラスがある。 RuleBasedDamagerRepairer および DefaultDamagerRepairer が該当するクラス である(表4)。 このうち、RuleBasedDamagerRepairer は DefaultDamagerRepairer を継承 しただけのクラスなので、実際には DefaultDamagerRepairer が実装クラスの実 体である。 このクラスは、IPresentationDamager と IPresentationRepairer の両方を実装 したクラスである。 IPresentationDamager と IPresentationRepairer は相互に強く依存しているた め、単一のクラスに両方の機能を実装したものと想定される。 DefaultDamagerRepairer はコンストラクタの引数として ITokenScanner など を必要とするが、これはパーティション内の更なるトークンの解析に利用される。 後述の Java エディタでもこの DefaultDamagerRepairer を使用している (フォント装飾の実装項参照)。 詳細を理解するためには Java エディタでの実装を読むのがよい。
表 4. IPresentationDamager と IPresentationRepairer の実装 クラス
名称 | 役割 |
---|---|
DefaultDamagerRepairer | DamagerとRepairerの両方の実装クラス。 |
RuleBasedDamagerRepairer | DefaultDamagerRepairerを継承しただけのクラス。 コンストラクタ以外の実装は持たない。 外部からの参照もない。 |
このクラスは、テキストのパーティション項で説明した ITokenScanner を 付与すると、この Scanner を利用して修正対象となる範囲を抽出し、その部分 の表示を修正させる。 修正範囲の表示内容の生成は AbstractTextEditor が保持する SourceCodeViewer に与えられた TextPresentation が行う (テキストエディタの構成項参照)。
テキストの整形に関する機能は org.eclipse.jface.text プラグインの org.eclipse.jface.text.formatter パッケージに用意されている。 formatter パッケージのクラスは下記の 3 つである。
表 5. テキストの整形に関するクラス
名称 | 種別 | 役割 |
---|---|---|
IFormattingStrategy | インタフェイス | テキスト整形の動作を定義すると共に、実際にテキスト整形処理を行うクラスの インタフェイス。 |
IContentFormatter | インタフェイス | IFormattingStrategy を管理するクラスのインタフェイス。 内容種別を与えると対応する IFormattingStrategy を返す。 |
ContentFormatter | クラス | IContentFormatter のテンプレート実装クラス。 |
このうち、テキスト修正動作に直接関連するものは IFormattingStrategy であ る。IFormattingStrategy インタフェイスには format というメソッドがある。 このメソッドは、整形対象の文字列や、そ の部分のインデント情報などを渡すと、その部分を整形した結果の文字列を返す。 整形対象や整形後の文字列の受け渡しには String を利用している。 このほかに、整形に時間を要するケースを想定してか、format の開始および停 止を通知するメソッドがある。
IFormattingStrategy については、テンプレート実装は用意されていない。 これは、FormattingStrategy クラスの実装は、ドキュメントの種類によって多 種多様で、極めて一般化しにくいためではないかと想定される。 なお、Java エディタでは独自に IFormattingStrategy の実装クラス JavaFormattingStrategy を用意している。
IContentFormatter インタフェイスは、テキストの内容種別に対して適切な IFormattingStrategy を提供するクラスである。 このインタフェイスの役割は、フォントの装飾機能における IPresentationReconciler インタフェイスと同様であり、内容種別文字列をキー とする HashMap のようなものである。 IContentFormatter のテンプレート実装が ContentFormatter クラスである。 フォント装飾での PresentationReconciler と同様に、基本的には ContentFormatter もエディタ毎の実装はあまり必要はなく、 Java エディタでも ContentFormatter がそのまま利用されている。
補完候補の表示に関する機能は org.eclipse.jface.text プラグインの org.eclipse.jface.text.contentassist パッケージに用意されている。 contentassist パッケージのクラスは非常に多いが、そのうち主要なクラスは下 記のとおりである。
表 6. 補完候補の表示に関するクラス
名称 | 種別 | 役割 |
---|---|---|
IContentAssistProcessor | インタフェイス | 入力された文字列に対して、提示すべき補完候補の一覧を管理・取得するクラス のインタフェイス。 また、メソッドなどを補完した最に提示するメソッドのパラメータの一覧情報も 提供する。 |
ICompletionProposal, ICompletionProposalExtension, ICompletionProposalExtension2 | インタフェイス | 1つ1つの補完候補の情報を保持するクラスのインタフェイス。 また、補完候補に関する説明文も保持する。 |
CompletionProposal | クラス | ICompletionProposalのテンプレート実装クラス。 |
IContextInformation, IContextInformationExtension | インタフェイス | メソッドなどを補完した際に、続けて提示されるメソッドのパラメータの一覧情 報等を管理するクラスのインタフェイス。 |
ContentInformation | クラス | IContextInformationのテンプレート実装クラス。 |
IContentAssistant | インタフェイス | IContentAssistProcessor を管理するクラスのインタフェイス。 内容種別を与えると対応する IContentAssistProcessor を返す。 |
ContentAssistant | クラス | IContentAssistantのテンプレート実装クラス。 |
補完候補は IContentAssistProcessor インタフェイスの実装が管理する。 実際の補完候補は、1つづつ ICompletionProposal インタフェイスのインスタ ンスに格納されており、IConentAssistProcessor の computeCompletionProposals メソッドを呼ぶことで ICompletionProposal の配 列を取得することができる。 メソッド補完時などに表示されるパラメータの一覧も IContentAssistProcessor が管理しており、こちらは IContextInformation に格納される。 これも同様に IContentAssistProcessor の compilteContextInformation メソッ ドを呼ぶことで、配列を取得することができる。 IContentAssistProcessor および IContextInformation はそれぞれテンプレート 実装が用意されている。 なお、IContentAssistProcessor のテンプレート実装は用意されていない。
IContentAssistant インタフェイスは、テキストの内容種別に対して適切な IContentAssistProcessor を提供するクラスである。 このインタフェイスの役割は、その他の機能と同様に、内容種別文字列をキー とする HashMap のようなものである。 IContentAssistant のテンプレート実装が ContentAssistant クラスで ある。なお、Java エディタでは ContentAssistant クラスと、このクラスをを 継承した JavaCorrectionAssistant の2つを利用している。
ポップアップの表示に関する機能は org.eclipse.jface.text プラグインの org.eclipse.jface.text.information パッケージに用意されている。 information パッケージのクラスは下記のとおりである。
表 7. ポップアップの表示に関連するクラス
名称 | 種別 | 役割 |
---|---|---|
IInformationProvider | インタフェイス | テキスト中の注目している場所情報から、ポップアップ表示すべき文字列を決定 する。 |
IInformationProviderExtension | インタフェイス | テキスト中の注目している場所情報から、ポップアップ表示すべき内容を文字列 ではなくオブジェクトで返す。 |
IInformationPresentor | インタフェイス | IInformationProvider を管理するクラスのインタフェイス。 内容種別を与えると対応する IInformationProvider を返す。 |
InformationPresentor | クラス | IInformationPresentor インタフェイスのテンプレート実装クラス。 |
もともとは、ポップアップの情報提示も org.eclipse.jface.text.contentassist パッケージで提供されていたが、org.eclipse.jface.text.information が後か ら追加されたようだ。現在でも org.eclipse.jface.text.contentassist にもポッ プアップに関連するクラスが残っており、こちらでもポップアップ表示が可能な ようである。 |
表示内容に直接関わるものは IInformationProvider インタフェイスと IInformationProviderExtension インタフェイスである。 IInformationProvider インタフェイスには getSubject というメソッドと getInformation というメソッドがある。 getSubject メソッドはテキスト中の指定されたポイントに対して、同じ情報が 表示される範囲を返すメソッドである。 例えば、Java ソースエディタを例に取ると、メソッド名の部分にマウスカーソ ルを移動させると、同じメソッド名の上をマウスカーソルが移動している間は、 そのメソッドに関するポップアップメッセージを表示し続けるが、メソッド名の 部分からマウスカーソルが外れるとポップアップメッセージは消滅する。 この場合、getSubject は、このメソッド名の開始位置から終了位置までを表示 範囲として返す。 もう一つの getInformation メソッドは、getSubject メソッドが返した表示範 囲に表示すべきメッセージを文字列 (String) で返す。 例えば、Java ソースエディタで、getSubject が指し示した表示範囲が文字列だっ た場合、getInformation メソッドはそこで表示すべき内容、つまり、メソッド の型定義と JavaDoc コメントの冒頭部分を戻り値として返す。 IInformationProviderExtension は、IInformationProvider の getInformation の戻り値を Object にした getInformation2 というメソッドを 持っている。これは、ポップアップ表示の内容が String だけでは表現しきれな いケースを想定して用意されたものである。
IInformationProvide および IInformationProviderExtension の テンプレート実装はワークベンチでは用意されていない。 Java エディタでは独自にこれらの実装クラス JavaElementProvider および JavaInformationProvider クラスを用意している。
IInformationPresenter インタフェイスは、テキストの内容種別に対して適切な IInformationProvider を提供するクラスである。 このインタフェイスの役割は、その他の機能と同様に、内容種別文字列をキー とする HashMap のようなものである。 IInformationPresenter のテンプレート実装が InformationPresenter クラスで ある。これもその他の機能と同様に、エディタ毎の実装はあまり必要はなく、 Java エディタでも InformationPresenter がそのまま利用されている。
外部のドキュメント情報との連携に関する機能は org.eclipse.jface.text プラグインの org.eclipse.jface.text.reconciler パッケージに用意されている。 reconciler パッケージの主要クラスは下記のとおりである。
表 8. 外部のドキュメント情報との連携に関連したクラス
名称 | 種別 | 役割 |
---|---|---|
IReconcilingStrategy | インタフェイス | 外部情報との連携処理を実行するクラスのインタフェイス。 |
IReconcilingStrategyExtension | インタフェイス | 主に時間がかかる処理を対象とした拡張機能のインタフェイス。 プログレスバーの表示に関するメソッドを持つ。 |
IReconciler | インタフェイス | IReconcilingStrategy を管理するクラスのインタフェイス。 内容種別を与えると対応する IReconcilingStrategy を返す。 |
外部のドキュメント情報との連携処理に直接関与するものは、 IReconcilingStrategy インタフェイスと IReconcilingStrategyExtension イン タフェイスである。 IReconcilingStrategy インタフェイスには reconcile というメソッドがあり、 このメソッド内にでドキュメントの変更に対する処理内容を記述する。 reconcile メソッドは2つあるが、片方が変更範囲が明らかな場合に変更され ている範囲を付与できるのにたいし、もう片方は変更範囲は特定せずに再構成を 行うメソッドである。 IReconcilingStrategyExtension には setProgressMonitor というメソッドがあ り、進捗状況を表示するプログレスバーなどと連携ができるようになっている。 これは、ドキュメント情報との整合性をとる処理には時間を要することが多いた めと思われる。
IReconciler インタフェイスは、テキストの内容種別に対して適切な IReconcilerStrategy を提供するクラスである。 このインタフェイスの役割は、その他の機能と同様に内容種別文字列をキー とするハッシュマップのようなものである。 IReconciler のテンプレート実装は用途に応じて複数用意されている。 実装クラス一覧を図10に示す。 Reconciler がフルスペックの実装であるのに対し、MonoReconciler では内容種 別によらず同じ IReconcilingStrategy を返す。 Java エディタでは後者の MonoReconicler を継承した JavaReconciler を実装 している。
本節では Eclipse の JDT (Java Development Toolkit) が提供する Java ソー スコードエディタについて説明する。
Eclipse は Java ソースファイル編集用のエディタとして Java エ ディタを提供している。Java エディタは通常のエディタ機能に加え て、Java に特化した下記の機能を持っている。
予約語、コメント、文字列などの文字色やフォントの変更
自動インデント
ソースコードの補完候補提示機能
クラスやメソッドの説明のポップアップ表示
自動コンパイルとエラー表示
その他諸々
Java エディタの実体は org.eclipse.jdt.ui プラグインで定義されている。ソー スファイルは、org.eclipse.jdt.ui プラグインの中の ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditor.java である。この JavaEditor 自体は、Java のソースファイルの基本的な表示および編集機能を提 供しているが、実装クラスではなく抽象クラスとなっている。実装クラスは、 JavaEditor クラスを継承した ClassFileEditor クラスと CompilationUnitEditor クラスである。ファイルはいずれも JavaEditor クラス と同じディレクトリにある。 CompilationUnitEditor が Java ソースファイル (.java ファイル)を編集対象としているのに対して、ClassFileEditor クラスは、 クラスファイル(.class ファイル)を対象としたものである。
org.eclipse.jdt.ui プラグインに含まれる Java エディタの中核クラスを 表9に示す。
表 9. Java エディタの中核クラス
名称 | パッケージ | 説明 |
---|---|---|
JavaEditor | org.eclipse.jdt.internal.ui.javaeditor | Java エディタ本体。StatusTextEditor を継承したクラスで、Java エディタに 関する機能はすべてこのクラスに集約されている。ただし、このクラスは抽象ク ラスであり、実装クラスは ClassFileEditor および CompilationUnitEditor の 2つがある。 |
ClassFileEditor | org.eclipse.jdt.internal.ui.javaeditor | Java エディタの実装クラス。JavaEditor を継承している。 主に、コンパイル済みのクラスファイルに対する編集機能を提供する。 |
ClassFileDocumentProvider | org.eclipse.jdt.internal.ui.javaeditor | IDocumentProvider の実装クラス。 エディタとクラスファイルとの連携を図る。 |
CompilationUnitEditor | org.eclipse.jdt.internal.ui.javaeditor | Java エディタの実装クラス。JavaEditor を継承している。 主に、Java ソースファイルに対する編集機能を提供する。 |
CompilationUnitDocumentProvider | org.eclipse.jdt.internal.ui.javaeditor | IDocumentProvider の実装クラス。 エディタと Java ソースファイルとの連携を図る。 |
JavaSourceViewer | org.eclipse.jdt.internal.ui.javaeditor | SourceViewer を継承したクラス。Java プログラムに特化したテキスト表示機能 を提供する。 |
JavaSourceViewerConfiguration | org.eclipse.jdt.ui.text | SourceViewerConfiguration を継承したクラス。Java ソースに特化した SourceViewer の設定を保持する。 |
JavaTextTools | org.eclipse.jdt.ui.text | パーティションスキャナ等のインスタンス等、 Java プログラムの分析に関連したクラスを一括して管理する。 このクラスはパーティションのスキャナだけではなく、Java のプログラムやコ メントなどの内部をスキャンするクラスも管理している。 これらのクラスはフォントの装飾などで利用されている。 |
本項では Java エディタで実装されている様々な Java 独自の機能について、 Eclipse ワークベンチのエディタフレームワークとの関連を踏まえて説明する。
本項では JavaEditor のパーティション分割に関連するクラスを概観する。 パーティション分割の詳細は、テキストのパーティション項を参照のこと。
Java エディタでは下記の 6 種類のパーティションを用意している。
表 10. Java エディタで定義されているパーティション
パーティション名 | パーティションの内容 |
---|---|
JAVA_SINGLE_LINE_COMMENT | "//" で始まる1行コメント。行末までが対象。 |
JAVA_MULTI_LINE_COMMENT | "/*" と "*/" で囲まれた複数行にまたがるコメント。 |
JAVA_DOC | 複数行コメントのうち "/**" と "*/" で囲まれた部分で、JavaDoc が記述され ているコメント。複数行にまたがることができる。 |
JAVA_STRING | " (ダブルクウォート)で囲まれた文字列。 基本的に複数行にまたがることはできない。 |
JAVA_CHARACTER | ' (シングルクウォート)で囲まれた文字。 複数行にまたがることはできない。 |
それ以外 | Java のプログラム本文 |
これらのパーティションの内容種別は IJavaPartitions インタフェイスで管理 している。
JavaEditor ではパーティションのスキャンには RuleBasedPartitionScanner な どは利用しておらず、独自にスキャナクラスを実装している。 これは、ルールベースのスキャナでは十分な性能が確保できなかったためではな いかと想定される。 なお、トークンについては JFace フレームワークの Token クラスをそのまま利 用している。 パーティションのスキャンに関するクラスは下記のとおりである。
本項では JavaEditor のフォント装飾の実装について概観する。 フレームワークが提供する フォント装飾機能の詳細は、フォントの装飾項を参照 のこと。 また、パーティション分割と内容種別については既に パーティション分割と内容種別の判定項で述べたとおりである。 ここでは、パーティションごとの内容種別を決定した後の動作について説明する。
Java エディタでは、 JFace フレームワークで用意されているテンプレート実装に対して特に拡張の実 装は行なっておらず、そのまま利用している。 IPresentationDamager および IPresentationRepairer は、 DefaultDamagerRepairer を、また IPresentationReconciler は PresentationReconciler をそれぞれ利用している。 Java エディタ独自の実装としては、IPresentationDamager および IPresentationRepairer で利用する、パーティション内のトークン分析用のスキャ ナ群である。 表12 に Java エディタが用意しているパー ティション内のトークンのスキャナクラスを示す。
表 12. Java エディタの色づけ関係
名称 | パッケージ | 説明 |
---|---|---|
JavaCodeScanner | org.eclipse.jdt.internal.ui.text.java | Java プログラムの本文をスキャンし、予約語などに分割するスキャナ |
JavaCommentScanner | org.eclipse.jdt.internal.ui.text | Java の複数行コメントおよび1行コメント部分のためのスキャナ。 特にたいした解析は行っていない。 |
JavaDocScanner | org.eclipse.jdt.internal.ui.text.javadoc | JavaDoc コメントに利用するためのスキャナ。 JavaCommentScanner の機能に加えて、xml タグ情報の取り出しや、@ で始まる 表記などを解析している。 |
SingleTokenJavaScanner | org.eclipse.jdt.internal.ui.text | Java プログラム中の文字列定数や文字定数部分に対して利用するスキャナ。 実装はほぼ皆無である。 |
Java エディタでは、これらのスキャナをそれぞれ DefaultDamageRepairer に与 えることにより、Java プログラム本体や、コメント部分、JavaDoc などに特化 した IPresentationDamager および IPresentationRepairer のインスタンスを 作成している。 Damager および Repairer のインスタンスの生成と、IPresentationReconciler への登録は、 org.eclipse.jdt.ui.text パッケージの JavaSourceViewerConfiguration クラスの getPresentationReconciler メソッドで行われている。 また、個々のスキャナのインスタンスは org.eclipse.jdt.ui.text パッケージの JavaTextTools クラスで一括管理され ている。
JDT のエラー表示はエディタとはかなり独立した機能として提供されている。 Java ソースのコンパイル結果は org.eclipse.jdt.core プラグインの dom ディ レクトリ内の org.eclipse.jdt.core.dom パッケージによって解析され、 エラーとして報告される。 個々のエラー情報は IProblem クラスに格納され、IProblem の配列で管理され る。
JDT のユーザインタフェイス部分では、core extension ディレクトリの org.eclipse.jdt.internal.corext.dom パッケージ等を解して、dom が生成した エラー情報を取得する。 これらのエラー情報は org.eclipse.jdt.internal.ui.text.correction パッケー ジでハンドリングされ、エディタの補完機能やマーカー表示などに反映される。
本項では JavaEditor のソースコード整形に関連するクラスを概観する。 なおソースコード整形の詳細は、テキストの整形に関するクラス項を参 照のこと。
JFace が提供する整形機能のうち、主要なインタフェイスは IFormattingStrategy と IContentFormatter の2種である。 このうち、IFormattingStrategy については テキストの整形に関するクラス項 で述べたようにテンプレートクラスが 用意されていないため、Java エディタでは独自の実装クラス JavaFormattingStrategy を実装している。 このクラスの実体は、org.eclipse.jdt.ui プラグインの org.eclipse.jdt.internal.text.java パッケージにある。 なお、JavaFormattingStrategy の内部では JDT のコア部分が用意するコード整 形クラスICodeFormatter のインスタンスを取得して、そちらに処理を依頼して いる。 ICodeFormatter は org.eclipse.jdt.core プラグインに属しており、 コア部分の保持している Java プログラムパーサを利用して整形を行っている。
なお、IContentFormatter については、JFace が用意したテンプレート実装をそ のまま利用している。 IContentFormatter のインスタンスは JavaSourceViewerConfiguration が保持 しており、getContentFormatter メソッドで取得できる。
本項では JavaEditor の補完候補提示に関連するクラスを概観する。 なお補完候補提示機能の詳細は、補完候補の表示項を参 照のこと。
補完候補の提示には、IContentAssistProcessor, ICompletionProposal, IContextInformation, IContentAssistant の各インタフェイスを実装する必要 がある。 IContentAssistProcessor は独自の実装クラスとして JavaCompletionProcessor, JavaDocCompletionProcessor および JavaCorrectionProcessor を実装している。 これらの詳細を表13に示す。
表 13. Java エディタのソースコード補完関係
名称 | パッケージ | 説明 |
---|---|---|
JavaCompletionProcessor | org.eclipse.jdt.internal.ui.text.java | IContentAssistProcessor の実装クラス。 Javaプログラムの本文、文字列定数および一行コメントに対する補完候補を提示 するクラス。 |
JavaDocCompletionProcessor | org.eclipse.jdt.internal.ui.text.javadoc | IContentAssistProcessor の実装クラス。 JavaDoc コメントに対する補完候補を提示するクラス。 |
JavaCorrectionProcessor | org.eclipse.jdt.internal.ui.text.correction | IContentAssistProcessor の実装クラス。 エラー修正等に関する表示にも対応。 |
このほかに、ICompletionProposal の実装クラス (図11)および IContextInformation の実装クラス (図12) が多数用意されている。 これらは数が膨大なため詳細は割愛する。
IContentAssistant については、JavaEditor ではテンプレート実装の ContentAssistant クラスをそのまま利用している。 しかし、JavaEditor の実装クラスである CompilationUnitEditor では、 ContentAssistant クラスを継承した JavaCorrectionAssistant を利用している。
JavaEditor が利用している ContentAssistant は JavaSourceViewerConfiguration が保持しており、getContentAssistant メソッ ドで取得できる。 一方で CompilationUnitEditor が利用する JavaCorrectionAssistant は CompletionUnitEditor の内部クラスの AdaptedSourceViewer で管理されてい る。
本項では JavaEditor のポップアップ表示機能に関連するクラスを概観する。 なおポップアップ表示の詳細は、ポップアップの表示項を参 照のこと。
JFace が提供するポップアップ機能は IInformationProvide インタフェイスと IInformationPresenter インタフェイス を実装することで利用できる。 このうち IInformationProvide については ポップアップの表示項でも述べたように JFace フレームワークではテンプレート実装は提供されていない。 そこで、Java エディタでは独自に JavaElementProvider および JavaInformationProvider の実装クラスを用意している。
表 14. Java エディタのポップアップ表示関係
名称 | パッケージ | 説明 |
---|---|---|
JavaElementProvider | org.eclipse.jdt.internal.ui.text | IInformationProvider インタフェイスの実装クラス。 基本機能のみを提供。プログラム本文および JavaDoc 部分のみに適用される。 |
JavaInformationProvider | org.eclipse.jdt.internal.ui.text.hover | IInformationProvider と IInformationProviderExtension の両方のインタフェ イスを実装したクラス。 ソースコードの全ての部分に適用されている。 |
なお、IInformationPresenter については、JFace が用意したテンプレート実装 をそのまま利用している。 IInformationPresenter のインスタンスは JavaSourceViewerConfiguration が 保持しており、getInformationPresenter および getOutlinePresneter メソッ ドで取得できる。 getInformationPresenter は JavaSourceViewerConfiguration の親クラス SourceViewerConfiguration で定義されているメソッドであり、JFace の SourceViewer の設定に利用される。 一方で getOutlinePresenter は JavaSourceViewerConfiguration の独自のメソッ ドであり、JFace フレームワークでは利用されない。 このメソッドは JavaSourceViewer から呼ばれており、Java エディタ独自のポッ プアップ表示に利用されている。
本節では Java エディタの拡張について説明する。ここでは、以前実装を行った 楽観的 try 文およびマップ初期化子に対する色づけを実装する。
実装についての説明の前に、Eclipse のビルド方法につて説明する。 本項では Eclipse 全体を一括でビルドする方法のほかに、特定のプラグイン のみをビルドする方法についても説明する。
ここでは Eclipse のソース全体から Eclipse のバイトコードを生成する方 法について述べる。ソースに改変を行った Eclipse のバイトコードを生成す るためには必ず一回はフルビルドを行う必要がある。
ソースツリーのフルビルドを行う際は、Eclipse ソースツリーのトップディレ クトリで下記のコマンドを実行する。
% cd ${eclipse} % build -os <OS種別> -ws <ウィンドウシステム種別> [ -bc <rt.jarのパス> ] |
ここで、${eclipse} は Eclipse のソースツリーのトップディレクトリである。 <rt.jarのパス>には Java のランタイムライブラリ(J2SDKに含まれる rt.jar)を指定する。なお、クロスコンパイルではない場合は、-bc オプション は不要である。 また、<OS種別>および<ウィンドウシステム種別>に指定する値を 表15に示す。
表 15. OSおよびウィンドウシステムの組み合わせ
OS種別 | ウィンドウシステム種別 |
---|---|
win32 | win32 |
linux | motif |
linux | gtk |
solaris | motif |
aix | motif |
hpux | motif |
qnx | photon |
Eclipse のソースは全体で数十MByte に及ぶため、Eclipse 全体をビルドす ると処理系によっては1時間近くかかってしまい、極めて効率が悪い。 そこで、フルビルドの必要がない場合は、可能な限り次項で述べるプラグインご とのビルドを行うことを推奨する。
ここでは特定のプラグインのみを単独でビルドする方法について説明する。 原則的には、個々のプラグインはそれほど密な関係にはないため、外部から 参照可能な API の仕様変更がない場合(プラグインの内部的な実装のみの変更の 場合)は、たいてい変更を行ったプラグインのみの再構築で動作する。 但し、この手順は全ての場合に適用可能できるわけではない。状況によってはプ ラグインのみのビルドでは適正に動作せず、全体をコンパイルし直す必要がある。
なお、本項で説明するのは厳密にはプラグイン単体でのビルドではなく、関連 性のあるプラグイン集合のビルドである。
# cd ${ECLIPSE}/features/org.eclipse.jdt-feature/ # ant clean # ant -buildfile build.xml -Dos=<OS種別> -Dws=<ウィンドウシステム種別> -Dbootclasspath=<rt.jarのパス> |
<OS種別>、 <ウィンドウシステム種別>、<rt.jarのパス>に 設定する値については前項と同じである(表15Eclipse のフルビルド項参照)。 2 行目で clean を実行しているが、これは、clean をせずにビルドした場合に うまく動作しないことがあったためである。実際に詳細な条件については調査を 行っていないが、改変内容によっては clean が必要となるようである。
本項では楽観的 try 構文に対するコードの拡張について説明する。
まずは、楽観的 try 構文について簡単に説明する。 なお、本節で対象とする楽観的 try 構文は前回の調査報告で実装を行ったもの と全く同じ仕様である。
楽観的 Try 構文は、API インタフェイス上は例外が発生し得るが、実際には例 外が発生し得ない場合のコーディングを楽にするシンタックスシュガーである。
ByteArrayOutputStream os = new ByteArrayOutputStream(); try __optimistic__ { os.close(); } |
これは、意味的には下記のコードと等価である。
ByteArrayOutputStream os = new ByteArrayOutputStream(); try { os.close(); } catch (Throwable th) { } |
つまり、__optimistic__を付加したtry文では、catch節やfinally節を省略する ことが可能である。
ここでは楽観的 try 構文に対して色付けの拡張を行う方法について説明する。
ここで実装する楽観的 try 構文の色付けの仕様は下記のとおりである。
"__optimistic__" を他の予約語と同様の色およびフォントで表示する。
図13 にサンプルのエディタ画面を示す。 このエディタでは、通常の Java エディタでは黒字で表示される __optimistic__ がその他の予約語と同じフォントで表示されている。
__optimistic__ を予約語と同じフォントで表示するためには、 Java のプログラム部分のフォント装飾を担当している JavaSourceViewerConfiguration の DefaultDamagerRepairer では JavaCodeScanner クラスを利用して予約語を判定している。 そこで、JavaCodeScanner クラスの予約語判定部分で "__optimistic__" を予約 語と判定するよう変更することで、 "__optimistic__" に色付けを行うことがで きる。
なお、今回対象となる Eclipse のソースツリーに対しては、前回の調査報告 に記述した楽観的 try 構文に関するコンパイラ拡張が既に適用されているもの とする。
変更が未適応でも楽観的 Try 構文の色づけ自体は行われるが、 パースに失敗するためエラーなどのマーカーが表示される。 |
JavaCodeScanner は org.eclipse.jdt.ui プラグインの ui ディレクトリの org.eclipse.jdt.internal.ui.text.java パッケージにある。 予約語の判定は createRules メソッド(139行目)の中でスタティックメンバの String[] fgKeywords (83行目) と比較することで行っている。 従って、fgKeywords に "__optimistic__" を追加してやればよい。 下記に変更したソースを示す。
... public final class JavaCodeScanner extends AbstractJavaScanner { ... private static String[] fgKeywords= { "abstract", //$NON-NLS-1$ "break", //$NON-NLS-1$ "case", "catch", "class", "const", "continue", //$NON-NLS-5$ //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ "default", "do", //$NON-NLS-2$ //$NON-NLS-1$ "else", "extends", //$NON-NLS-2$ //$NON-NLS-1$ "final", "finally", "for", //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ "goto", //$NON-NLS-1$ "if", "implements", "import", "instanceof", "interface", //$NON-NLS-5$ //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ "native", "new", //$NON-NLS-2$ //$NON-NLS-1$ "package", "private", "protected", "public", //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ "return", //$NON-NLS-1$ "static", "super", "switch", "synchronized", //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ "this", "throw", "throws", "transient", "try", //$NON-NLS-5$ //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ "volatile", //$NON-NLS-1$ "while", //$NON-NLS-1$ // ************** for optimistic try ************** "__optimistic__" // ************************************************* }; private static String[] fgNewKeywords= { "assert" }; //$NON-NLS-1$ ... |
以上の変更を適用した上で Eclipse を再構築することで、 楽観的 Try に対応した Eclipse が生成される。
このように、予約語の追加だけであれば容易に対応できる。 しかしながら、色付けに関連した文法規則がコンパイラとは独立して実装されて いるため、文法拡張と併用した場合に、拡張された文法とエディタ関係の拡 張部分との対応関連をどのようにして管理するかが課題である。
本項ではマップ初期化子構文に関する Eclipse エディタの拡張について説明す る。
マップ初期化子は、既に値が登録された Map インスタンスを初期化する構文を 提供するものである。
Map colors = %{ "red" => new Integer(0xff0000), "green" => new Integer(0x00ff00), "blue" => new Integer(0x0000ff) }; |
上記のように記述すると、下記のコードと同じ動作をする。
Map colors = new HashMap(); colors.put("red", new Integer(0xff0000)); colors.put("green", new Integer(0x00ff00)); colors.put("blue", new Integer(0x0000ff)); |
ここではマップ初期化子部分についての色付けを行う。
ここで実装するマップ初期化子の色付けの仕様は下記のとおりである。
マップ初期化子の部分に対して複数行コメントと同様の色付けを行う。
マップ初期化子の範囲は %{ から、直近の } までの区間であるものとする。
図14 にサンプルのエディタ画面を示す。 このエディタでは、%{ から } までの部分がコメントと同様の薄い茶色で表現さ れている。
ここでは、マップ初期化子部分を通常の Java プログラムとは異なる部分と見立 てて、この部分を切り出す方法を採用する。 つまり、マップ初期化子部分を切り出し、この部分を独立したパーティションと して新たな内容種別を付与する。
マップ初期化子は Java のプログラムコードの一部であり、その間にコメ ント等が出現しうるので、ここで説明したような新たなパーティションとして切 り出す手法は適切ではない。 しかしながら、今回は pxml を実装した場合を想定し、新たなパーティションを 追加する方法の事前調査という位置づけで、あえてマップ初期化子部分を Java プログラム本文とは異なるパーティションとして切り出す方法を選択した。 |
パーティションの範囲および内容種別の判定は JavaPartitionScanner が行って いる。今回はさらに IJavaPartitions にパーティションの内容種別を追加した。 ただし、実際に内容種別を追加してしまうと、新たに追加されたパーティション 種別に対して、フォント装飾等の動作クラスを実装しなければならず、影響範囲 が大きいため、内容種別の定義文字列はコメントと同様のものを利用することと した。
今回対象となる Eclipse のソースツリーは、前回の調査報告 に記述したマップ初期化子構文に関するコンパイラ拡張が既に適用されているも のとする。
変更が未適応の場合でもマップ初期化子構文の色づけは行われるが、 パースに失敗するためエラーなどのマーカーが表示される。 |
変更対象ファイルは、 org.eclipse.jdt.ui プラグインの ui ディレクトリの org.eclipse.jdt.internal.ui.text パッケージにある、 FastJavaPartitionScanner および IJavaPartitions である。
IJavaPartitions には新たなパーティション内容種別として JAVA_MAP_INITIALIZER を追加する。ただし、ここでは実体は JAVA_MULTI_LINE_COMMENT と同じ物を代入する。 ソースは下記のとおりである。
... public interface IJavaPartitions { public final static String JAVA_SINGLE_LINE_COMMENT= "__java_singleline_comment"; //$NON-NLS-1$ public final static String JAVA_MULTI_LINE_COMMENT= "__java_multiline_comment"; //$NON-NLS-1$ public final static String JAVA_DOC= "__java_javadoc"; //$NON-NLS-1$ public final static String JAVA_STRING= "__java_string"; //$NON-NLS-1$ public final static String JAVA_CHARACTER= "__java_character"; //$NON-NLS-1$ // ************** for map initializer ************** public final static String JAVA_MAP_INITIALIZER= JAVA_MULTI_LINE_COMMENT ; // ************************************************* } |
続いて、実際にスキャンしている部分である FastJavaPartitionScanner を修正 する。 ここでの修正点は下記のとおりである。
スタティック変数の追加
クラス冒頭部分にスタティック変数を追記する。
... public class FastJavaPartitionScanner implements IPartitionTokenScanner, IJavaPartitions { // states private static final int JAVA= 0; private static final int SINGLE_LINE_COMMENT= 1; private static final int MULTI_LINE_COMMENT= 2; private static final int JAVADOC= 3; private static final int CHARACTER= 4; private static final int STRING= 5; // ************** for map initializer ************** private static final int MAP_INITIALIZER = 6; // ************************************************* // beginning of prefixes and postfixes private static final int NONE= 0; private static final int BACKSLASH= 1; // postfix for STRING and CHARACTER private static final int SLASH= 2; // prefix for SINGLE_LINE or MULTI_LINE or JAVADOC private static final int SLASH_STAR= 3; // prefix for MULTI_LINE_COMMENT or JAVADOC private static final int SLASH_STAR_STAR= 4; // prefix for MULTI_LINE_COMMENT or JAVADOC private static final int STAR= 5; // postfix for MULTI_LINE_COMMENT or JAVADOC private static final int CARRIAGE_RETURN=6; // postfix for STRING, CHARACTER and SINGLE_LINE_COMMENT // ************** for map initializer ************** private static final int PERCENT=7; private static final int PLUS=8; // ************************************************* /** The scanner. */ private final BufferedDocumentScanner fScanner= new BufferedDocumentScanner(1000); // faster implementation ... |
IToken[] fTokens (63行目) に上記で追加したスタティック変数を追加
... private final IToken[] fTokens= new IToken[] { new Token(null), new Token(JAVA_SINGLE_LINE_COMMENT), new Token(JAVA_MULTI_LINE_COMMENT), new Token(JAVA_DOC), new Token(JAVA_CHARACTER), new Token(JAVA_STRING), // ************** for map initializer ************** new Token(JAVA_MAP_INITIALIZER) // ************************************************* }; ... |
nextToken メソッド(75行目) のキャラクタの判定部分にパーセントを判定する コードを追加(190行目近辺)
... public IToken nextToken() { ... while (true) { ... switch (ch) { ... default: if (!fgEmulate && fLast == CARRIAGE_RETURN) { switch (fState) { case SINGLE_LINE_COMMENT: case CHARACTER: case STRING: int last; int newState; switch (ch) { case '/': last= SLASH; newState= JAVA; break; case '*': last= STAR; newState= JAVA; break; case '\'': last= NONE; newState= CHARACTER; break; case '"': last= NONE; newState= STRING; break; // ************** for map initializer ************** case '%': last= PERCENT; newState= JAVA; break; // ************************************************* case '\r': last= CARRIAGE_RETURN; newState= JAVA; break; case '\\': last= BACKSLASH; newState= JAVA; break; default: last= NONE; newState= JAVA; break; } fLast= NONE; // ignore fLast return preFix(fState, newState, last, 1); default: break; } } } ... |
nextToken メソッド(75行目) の Java プログラム中に出現するパーティション 開始位置の判定部分にコードを追加(252行目近辺)
... // states switch (fState) { case JAVA: switch (ch) { case '/': ... case '*': if (fLast == SLASH) { ... } else { consume(); break; } // ************** for map initializer ************** case '%': fTokenLength++; fLast= PERCENT; break; case '{': if (fLast == PERCENT) { if (fTokenLength - getLastLength(fLast) > 0) return preFix(JAVA, MAP_INITIALIZER, PLUS, 2); else { preFix(JAVA, MAP_INITIALIZER, PLUS, 2); fTokenOffset += fTokenLength; fTokenLength= fPrefixLength; break; } } else { consume(); break; } // ************************************************* case '\'': fLast= NONE; // ignore fLast if (fTokenLength > 0) return preFix(JAVA, CHARACTER, NONE, 1); else { preFix(JAVA, CHARACTER, NONE, 1); fTokenOffset += fTokenLength; fTokenLength= fPrefixLength; break; } case '"': ... default: consume(); break; } break; case SINGLE_LINE_COMMENT: consume(); break; case JAVADOC: ... |
nextToken メソッド(75行目) にマップ初期化子区間の終了判定の条件を追加 (337行目近辺)
... case JAVADOC: ... case MULTI_LINE_COMMENT: switch (ch) { case '*': if (fLast == SLASH_STAR) { fLast= SLASH_STAR_STAR; fTokenLength++; fState= JAVADOC; } else { fTokenLength++; fLast= STAR; } break; case '/': if (fLast == STAR) { return postFix(MULTI_LINE_COMMENT); } else { consume(); break; } default: consume(); break; } break; // ************** for map initializer ************** case MAP_INITIALIZER: switch (ch) { case '}': return postFix(MAP_INITIALIZER); default: consume(); break; } break; // ************************************************* case STRING: switch (ch) { case '\\': fLast= (fLast == BACKSLASH) ? NONE : BACKSLASH; fTokenLength++; break; case '\"': if (fLast != BACKSLASH) { return postFix(STRING); } else { consume(); break; } default: consume(); break; } break; case CHARACTER: ... } } } ... |
getState メソッド(442行目) の内容種別判定部分にマップ初期化子に関するも のを追加(461行目近辺)。
... private static final int getLastLength(int last) { switch (last) { default: return -1; case NONE: return 0; case CARRIAGE_RETURN: case BACKSLASH: case SLASH: case STAR: // ************** for map initializer ************** case PERCENT: // ************************************************* return 1; case SLASH_STAR: return 2; case SLASH_STAR_STAR: return 3; } } ... |
以上の変更を適用した上で Eclipse を再構築することで、 マップ初期化子に対応した Eclipse が生成される。 なお詳細は、添付のソースを参照されたし。
今回の実装方法には下記のような問題点が存在する。
今回の方法では、パーティションの内容種別は追加したが、内容種別文字列は 便宜のために複数行コメントと同じ JAVA_MULTI_LINE_COMMENT を利用している。 この弊害として、外部から setPartitionRange メソッドを呼ばれた際に、 指定された箇所がもともと複数行コメントとマップ初期化子の何れであったかが 判別できず、無条件に複数行コメントと判断するようになっている (getState メソッドで JAVA_MAP_INITIALIZER と比較する部分を追加したが、実 際にはここ以前の JAVA_MULTI_LINE_COMMENT と比較している部分でマッチして しまっている)。 従って、マップ初期化子であるにもかかわらず複数行コメントと判定されてしま い、編集等を行った際に } 以降も複数行コメントの色で表示されるようになっ てしまうことがある。
ここでは、マップ初期化子に対して複数行コメントと同じ内容種別を割り当てる ことで、フォントの色付けを行っているが、先に述べたとおり、マップ初期化子 の中にはコメントや JavaDoc なども登場しうる。 しかしながら、今回の方法ではマップ初期化子全体をコメントと解釈してしまう ため、マップ初期化子中に現れるコメントや JavaDoc は適性には描画されなく なってしまう。
今回は、フォント装飾のみを対象としているが、変更を行った部分はパーティショ ンをスキャンする部分であり、ここへの変更はフォント装飾以外の JFace が 提供する機能 (ポップアップ情報表示やコード補完) などにも適用される。 つまり、この変更によりポップアップ表示などの動作も通常の Java プログラム 部分ではなくコメント部分に対する動作が適用されてしまう。
この実装では、手間等の兼ね合いから新たな内容種別を加えることをせず、 いわば既存の機能を間借りした状態での実装となっている。 しかしながら、pxml など本格的に新たな内容種別を導入する場合、 今回編集したクラスのほかに、実際の動作を定義する諸クラスを足す用意する必 要がある。 IJavaPartitions に内容種別を加えることからはじまり、 新たに追加した内容種別に対応する IPresentationDamagerRepairer, IInformationProvider, IFormattingStrategy 等、沢山のクラスを作成もしくは 修正する必要がある。
大規模なJavaプログラムをMixJuiceに移植するのに備え、移植に必要な 作業を調査した。本章ではその調査結果やそこで得られたノウハウを述べる。
以下の4つのJavaプログラムをMixJuiceに移植する作業を通じて、移植に必要な作業を洗い出すこととする。
例 1. src/IJavaTest.java
package src; import java.io.IOException; public interface IJavaTest { final static int MIN = 0; final static int MAX = 100; final static int MID = (MAX + MIN) / 2; // (1) void doIO() throws IOException; IJavaTest intern(); boolean equals(Object o); // (2) } |
例 2. src/IJavaTest2.java
package src; public interface IJavaTest2 extends IJavaTest { void foo(); // (1) } |
例 3. src/Other.java
package src; public class Other { public String toString() { return this.getClass().getName(); } } |
例 4. src/JavaTest.java
package src; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import other.Other; public class JavaTest implements IJavaTest2, Serializable { // (1) protected static int i = 0; // (2) private static final Other other = new Other(); // (3) private int x; protected transient int xx; public static class IOException extends java.io.IOException { // (4) int errorI = i; // (5) public String toString() { return super.toString() + "errorI: " + errorI; } } public static int getMax() { // (6) return MAX; } protected JavaTest() throws InternalError { // (7) x = i++; xx = x * x; if (x % 2 == 0) throw new InternalError(); } public static JavaTest getInstance() // (8) throws InstantiationException, IllegalAccessException { return (JavaTest) JavaTestSub.class.newInstance(); // (9) } public void finalize() throws Throwable { // (10) super.finalize(); System.out.println("fin: " + x); } public IJavaTest intern() { return this; } public void doIO() throws IOException { // (11) throw new IOException(); } public void foo() { } private void writeObject(ObjectOutputStream s) throws java.io.IOException { // (12) s.defaultWriteObject(); } private void readObject(ObjectInputStream s) // (13) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); xx = x * x; } public String toString() { return "x: " + x + ", xx: " + xx; } public static void main(String[] args) { try { test1(test0()); } catch (Exception e) { e.printStackTrace(); System.exit(1); } System.out.println(other); } public static JavaTest test0() { JavaTest last = null; for (int i = 0; i < MID; i++) { try { IJavaTest j = getInstance().intern(); ((IJavaTest2) j).foo(); JavaTest jt = (JavaTest) j; jt.getMax(); // (14) last = jt; } catch (InternalError e) { } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } try { last.doIO(); } catch (IOException e) { e.printStackTrace(); } System.gc(); return last; } private static void test1(JavaTest corg) throws Exception { final String file = "c:/removeMe.tmp"; try { FileOutputStream fo = new FileOutputStream(file); ObjectOutputStream so = new ObjectOutputStream(fo); so.writeObject(corg); so.flush(); so.close(); } catch (Exception e) { e.printStackTrace(); System.exit(1); } System.out.println("original: " + corg); JavaTest cnew; try { FileInputStream fi = new FileInputStream(file); ObjectInputStream si = new ObjectInputStream(fi); cnew = (JavaTest) si.readObject(); si.close(); System.out.println("new: " + cnew); } catch (Exception e) { e.printStackTrace(); System.exit(1); } } } class JavaTestSub extends JavaTest { private int x; // (15) JavaTestSub() { x = xx / 2; } public String toString() { return super.toString() + ", sub.x: " + x; } } |
ここでは、実際にJavaのソースコードをMixJuiceに移植するのに必要な作業を列挙する。
MixJuiceでは、クラスのメンバ名がモジュール間で重複しないように、FQNの「.」を「_」を置き換えたJava識別子に変換される(例:modモジュールで定義されたmというメンバであれば、mod_m)。しかし、MixJuiceで書かれたコードをJavaから参照することを考慮すると、クラスのメンバ名がJavaから見て単純名(モジュール名のプレフィックスが付かないもの)で参照できたほうがよい。mjcに「-J-Dmjc.noFQN=1」オプションを付けることにより、単純名でメンバ名を参照できるようになる。
ただし、上記のオプションを付けると、モジュールを複数組み合わせる際、メンバ名が衝突する可能性があるので注意が必要である。
またMixJuiceのエントリーポイントを提供するmj.lang.ss.SSクラスのinstanceフィールドは、上記のオプションをつけずにコンパイルしたものがjarファイルに収められているため、上記のオプションを付けてコンパイルしたプログラムからは通常の方法では参照できない。例6のようにリフレクションを利用して参照する。
mj.lang.ss.SS.instance初期化のタイミング | |
---|---|
mj.lang.ss.SS.instanceはスタティック初期化子で初期化されるのではなく、public void main(String[])の実行が始まってから初期化される。したがって、例6のSS.init()メソッドをスタティック初期化子から呼び出してはならない。 |
例 6. mj.lang.ss.SS.instanceの参照方法
module mod { class SS { static SS singleton; define void init() { try { java.lang.reflect.Field f = Class.forName("mj.lang.ss._Delta_mj_lang_ss_SS").getDeclaredField("mj_lang_ss_instance"); Object o = f.get(null); singleton = (SS)o; } catch (SecurityException e) { e.printStackTrace(); assert false; } catch (NoSuchFieldException e) { e.printStackTrace(); assert false; } catch (IllegalArgumentException e) { e.printStackTrace(); assert false; } catch (IllegalAccessException e) { e.printStackTrace(); assert false; } catch (ClassNotFoundException e) { e.printStackTrace(); assert false; } } void main(String[] args) { init(); // (1) System.out.println(singleton); // (2) } } } |
MixJuiceのモジュールはJavaのパッケージに変換されるので、JavaのコードをMixJuiceに移植する際、パッケージ名と同名のモジュールを用意し、そのモジュールの中にクラスやインタフェイスを入れる。Javaのpackage文は除去する。その際、複数のJavaソースファイルを一つのMixJuiceソースファイルにまとめる必要がある。
MixJuiceではimport文が存在しないため、JavaのソースをMixJuiceのソースに移植する際、import文を除去しなければならない。しかし、MixJuiceの他のモジュールで書かれたクラス等を参照するには、必ずimports/usesを使用してインポートしなければならない(JavaのようにFQNで参照できない(Javaのクラスはimports/usesしなくてもFQNで参照できる))。
MixJuiceでは名前の衝突が起きやすい | |
---|---|
MixJuiceでは、インポートした名前は、インポートしたモジュールのサブモジュールでも自動的にインポートされるため、名前の衝突が起きやすい。例えば、モジュールM1が実装のために(インタフェイスには使用しない)モジュールMXをインポートしたとする。M1のサブモジュールM2で、MX内で定義されたクラスなどと同じ名前の別クラスなどを、単純名で参照できなくなる(FQNを使えばよい)。 Javaのクラス等はインポートなしでもFQNで参照できるので、インタフェイスに現れない(実装に使用する)Javaのクラス等は、インポートしない方がよい。Eclipseを使用する場合、インポートして単純名でコーディングするのが普通なので、その場合、多くの単純名をFQNにしなければならない。 |
MixJuiceではデフォルトのコンストラクタを自動生成しないので、Javaのデフォルトのコンストラクタ自動生成に依存したコードがある場合は、明示的にデフォルトコンストラクタを記述する。
MixJuiceではスタティックメソッドを定義できない。インスタンスメソッドとして実装しなければならない。つまり、呼び出しにはインスタンスが必須となる。そこで、例7のように補助クラス(StaticHolder)を使用し、唯一のインスタンス(StaticHolder.instance)をスタティックフィールドやスタティックメソッドのコンテナとして使用する。
スタティックメソッドの呼び出し部分は、ClassName.staticMethod()という構文だけではなく、instance.staticMethod()という構文でstaticメソッドを呼び出している部分も忘れずに変更しなければならない。
MixJuiceではスタティックメソッドを定義できないので、スタティックメソッドをインターフェイスとして提供するJavaパッケージを、そのパッケージを参照しているJavaコードの変更なしで、MixJuice化することはできない。
例 7. スタティックメソッドの移植例
module mod { define class StaticHolder { // (1) int staticData = 1; define StaticHolder() {} define synchronized int get() { return staticData; } define synchronized void incr() { staticData += NonStatic.DELTA; // (2) } } define class NonStatic { static final int DELTA = 1; static final StaticHolder staticHolder = new StaticHolder(); define NonStatic() {} define int foo() { return staticHolder.get() * 10; // (3) } } class SS { void main(String[] args) { System.out.println(new NonStatic().foo()); NonStatic.staticHolder.incr(); System.out.println(new NonStatic().foo()); } } } |
スタティックフィールド/メソッドのコンテナは非シリアライザブルに | |
---|---|
スタティックフィールド/メソッドのコンテナは、唯一のインスタンスでなければならないので、シリアライザブルなどにしてはならない。後述の理由で、private Object readResolve(Object)を定義できないので、シリアライザブルなどにすると唯一性を保証できない。読み込んだ際に、もう一つインスタンスができてしまう。 |
スタティックフィールド/メソッドのコンテナは排他の単位にもなる | |
---|---|
MixJuiceではすべてのメソッドがpublicで参照関係の制約が無いため、スタティックフィールド/メソッドのコンテナは何個に分割してもいいし、全部一つにまとめてもいい。しかし、synchronizedなスタティックメソッドがあった場合、コンテナは排他制御の単位にもなるので、適切な粒度でコンテナを用意すべきである。もちろん、可読性も重要である。 |
スタティックフィールド/メソッドのコンテナはスタティックに初期化 | |
---|---|
Javaのコードと同じ動作にするには、スタティックフィールド/メソッドのコンテナインスタンスをスタティックに初期化した方がよい。そのため例外が発生しないコンストラクタを用意するか、スタティック初期化子で初期化(例外をcatchする)しなければならない。コンテナインスタンスを参照するためのスタティックフィールドはfinalにする。mj.lang.ss.SS.instanceは初期化のタイミングが遅すぎるので、一般的には使わない方がよい。 |
MixJuiceで記述したコンストラクタは、Javaに変換された際インスタンスメソッドに変換される。そのため、リフレクションを使用してインスタンスを作るようなJavaコードをMixJuiceに移植する際、インスタンス生成後明示的にMixJuiceのコンストラクタを呼び出す必要がある。MixJuiceのコンストラクタ名は、クラス名の前に「_init_」というプレフィックスをつけた名前のJavaインスタンスメソッドになる。
MixJuiceでは内部クラスをサポートしていないので、内部クラスを外部に出さなければならない。それに伴い、内部クラスとそれを含んでいたクラス間の名前空間の関係がなくなるので、外部化された内部クラス内の、外部(outer)クラスに依存した参照を変更しなければならない。
親クラスのprotectedなメンバの参照 | ||||
---|---|---|---|---|
MixJuiceで定義するクラスMの親クラスJがJavaで実装されており、JがprotectedなメンバPMを持っている場合、Mの内部クラスIからはPMを参照できる。しかし、MixJuiceでは内部クラスを使用できないので、Iを外部化するとPMを参照できなくなる。実際には、MixJuiceコードのコンパイル時に、Javaコンパイラがエラーを出力する。例8と例9のJavaコードの内、例9だけMixJuice化すると例10のようになる。 例 8. MixJuiceに移植するJavaコードが参照するMixJuice化しないJavaパッケージ
例 9. MixJuiceに移植するJavaコード
例 10. MixJuiceに移植したコード
|
親クラスのメソッドを参照するのに使用しているJavaの「super.メソッド名(引数リスト)」を、「original(引数リスト)」に書き換える。
Javaで実装された親クラスの引数付きコンストラクタを呼べない | |||
---|---|---|---|
MixJuiceでは、Javaで実装された親クラスの引数付きコンストラクタを呼び出せないので、 Javaで記述された例11、例12のうち、例12のみMixJuiceに移植するのは難しい。 例 11. MixJuiceに移植するJavaコードが参照するMixJuice化しないJavaパッケージ
例 12. MixJuiceに移植したいJavaコード
|
親クラスの別のメソッドを呼び出せない | |
---|---|
「super.メソッド名(引数リスト)」を「original(引数リスト)」に置き換えるということは、メソッド名を指定できないことを意味し、例12の(2)に示したように、親クラスの別のメソッドを呼び出せない。 |
同名フィールドを区別できない | ||
---|---|---|
同一モジュールで定義された継承関係を持つ二つのクラスで、同名のインスタンスフィールドがあった場合、それらのフィールドを参照し分けることができない。 例 13. 同名フィールドを区別できない
|
MixJuiceでは「C.class」という構文をサポートしていないので、「Class.forName("C")」という書き方に変えなければならない。それに伴い、「java.lang.ClassNotFoundException」をハンドリングしなければならなくなる。クラス名を変更したり、ミスタイプすることもあるので、この例外を無視するのは好ましくない。
MixJuiceのinterfaceで定義したメソッドは、それを実装するクラスのスーパークラスで同一シグネチャのメソッドが定義されていても、再度実装しなければならない。 例えば、例1Javaのサンプルプログラム項の(2)の例では、equalsメソッドはObject型で実装されているため、Javaでは、IJavaTestインタフェイスを実装するクラスでequalsメソッドを実装する必要はない。しかし、MixJuiceでは実装する必要がある。MixJuiceでinterface定義時に、「boolean FQN[java::equals](Object o);」と記述できれば、この問題は回避できるかもしれない。
JavaプログラムをMixJuiceに移植する章の冒頭で示した例1Javaのサンプルプログラム項、例2Javaのサンプルプログラム項、例3Javaのサンプルプログラム項、例4Javaのサンプルプログラム項、例5Javaのサンプルプログラム項のJavaプログラムをMixJuiceに移植した結果を例14に示す。ただし、後述のように一部移植不可能な部分(シリアライズや他モジュールと同名のクラス定義など)があるため、オリジナルのJavaプログラムと同じ振る舞いにはなっていない。
例 14. mj-port.java
module other { define public class Other { define Other() {} public String toString() { return this.getClass().getName(); } } } module src uses other { /* 移植不可能 define public class FQN[src::Other] { define Other() {} public String toString() { return this.getClass().getName(); } } */ define public interface IJavaTest { final static int MIN = 0; final static int MAX = 100; final static int MID = (MAX + MIN) / 2; define void doIO() throws java.io.IOException; define IJavaTest intern(); define boolean equals(Object o); } define public interface IJavaTest2 extends IJavaTest { define void foo(); } define public class IOException extends java.io.IOException { int errorI = JavaTest.staticHolder.i; define IOException() {} public String toString() { return original() + "errorI: " + errorI; } } define class JavaTestStatic { int i = 0; /* 移植不可能 * final other.Other other = new other.Other(); */ define JavaTestStatic() {} define public static int getMax() { return JavaTest.MAX; } define public JavaTest getInstance() throws InstantiationException, IllegalAccessException { JavaTestSub jt = null; try { jt = (JavaTestSub)Class.forName("src.JavaTestSub").newInstance(); } catch (ClassNotFoundException e) { assert false; } jt._init_JavaTestSub(); return jt; } define public JavaTest test0() { JavaTest last = null; for (int i = 0; i < JavaTest.MID; i++) { try { IJavaTest j = getInstance().intern(); ((IJavaTest2) j).foo(); JavaTest jt = (JavaTest) j; getMax(); last = jt; } catch (InternalError e) { } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } try { last.doIO(); } catch (IOException e) { e.printStackTrace(); } System.gc(); return last; } define private void test1(JavaTest corg) throws Exception { final String file = "c:/removeMe.tmp"; try { java.io.FileOutputStream fo = new java.io.FileOutputStream(file); java.io.ObjectOutputStream so = new java.io.ObjectOutputStream(fo); so.writeObject(corg); so.flush(); so.close(); } catch (Exception e) { e.printStackTrace(); System.exit(1); } System.out.println("original: " + corg); JavaTest cnew; try { java.io.FileInputStream fi = new java.io.FileInputStream(file); java.io.ObjectInputStream si = new java.io.ObjectInputStream(fi); cnew = (JavaTest) si.readObject(); si.close(); System.out.println("new: " + cnew); } catch (Exception e) { e.printStackTrace(); System.exit(1); } } } define public class JavaTest implements IJavaTest2, java.io.Serializable { static final JavaTestStatic staticHolder = new JavaTestStatic(); private int x; protected transient int xx; define protected JavaTest() throws InternalError { x = staticHolder.i++; xx = x * x; if (x % 2 == 0) throw new InternalError(); } boolean FQN[src::equals](Object o) { return original(o); } public void finalize() throws Throwable { original(); System.out.println("fin: " + x); } public IJavaTest intern() { return this; } public void doIO() throws IOException { throw new IOException(); } public void foo() { } define private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); } define private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); xx = x * x; } public String toString() { return "x: " + x + ", xx: " + xx; } } class SS { public void main(String[] args) { try { JavaTest.staticHolder.test1(JavaTest.staticHolder.test0()); } catch (Exception e) { e.printStackTrace(); System.exit(1); } /* 移植不可能 * System.out.println(JavaTest.staticHolder.other); */ } } define class JavaTestSub extends JavaTest { private int sub_x; define JavaTestSub() { sub_x = xx / 2; } public String toString() { return original() + ", sub.x: " + sub_x; } } } |
ここでは、前述したもの以外に、Javaで記述されたコードをMixJuiceに移植する際、問題となり得る事柄をいくつか説明する。
Javaで他のパッケージを参照することはよくあるが、 MixJuiceでは、次のようなコードのいずれも、コンパイルすることができなかった。
例 15. 他モジュールの参照1
module other { define public class Other { define Other() {} public String toString() { return this.getClass().getName(); } } } module src uses other { define public class Other { // (1) define Other() {} public String toString() { return this.getClass().getName(); } } class SS { void main(String[] args) { System.out.println(JavaTestStatic.other); } } define class JavaTestStatic { static final FQN[other::Other] other = new FQN[other::Other](); } } |
FQN[src::Other]と変更してもエラー
例 16. 他モジュールの参照2
module other { define public class Other { define Other() {} public String toString() { return this.getClass().getName(); } } } module src imports other { define public class Other { define Other() {} public String toString() { return this.getClass().getName(); } } class SS { void main(String[] args) { System.out.println(JavaTestStatic.other); } } define class JavaTestStatic { static final FQN[other::Other] other = new FQN[other::Other](); // (1) } } |
other.Otherと変更してもエラー
Otherにすると、src.Otherが参照される。
例 17. 他モジュールの参照3(プリプロセス中にNullPointerExceptionでコンパイル停止)
module other { define public class Other { public String toString() { return this.getClass().getName(); } } } module src uses other { define public class FQN[src::Other] { public String toString() { return this.getClass().getName(); } } class SS { void main(String[] args) { System.out.println(JavaTestStatic.other); } } define class JavaTestStatic { static final FQN[other::Other] other = new FQN[other::Other](); } } |
MixJuiceではメソッドがすべてpublicになってしまう。しかし、オブジェクトのシリアライズを行う際のreadObjectメソッドなどは、privateであることに意味がある。したがって、MixJuiceではオブジェクトのシリアライズなどに関して、独自の制御を行うことができない。
一部のツール(JUnitやGUIビルダなど)は、リフレクションを利用してpublicなメソッドを呼び出す。その際、本来publicな扱いではないメソッドも呼び出されるために不都合が生じることがある。
private void testFoo1() {...} private void testFoo2() {...} public void testBar() {testFoo1(); testFoo2();} |
例えば、上記のJUnit用のコードは下記のように書き換えなければならない。
define void privateTestFoo1() {...} define void privateTestFoo2() {...} define void testBar() {privateTestFoo1(); privateTestFoo2();} |
JavaDocに相当するドキュメンテーションツールが無いので、ブラックボックスとしてMixJuiceクラスを利用したい場合でも、ソースを見る必要がある。また、モージュールを拡張した場合に、ドキュメントの差分をどう表現するかの検討が必要である。