« 2004年8月 | トップページ | 2004年10月 »

2004.09.27

ココログブックス・コンテスト参戦!

ココログブックス・コンテスト開催!」ということで,このEclipseプラグイン開発ブログも参戦(応募)してみました。
知り合いからは「おまえんとこのサイトはブログじゃない!」とか言われてますが,「こんなブログの利用法もあっていいじゃん♪」と思いますし,まぁ「参加することに意義がある」ということで,お許しくださいませ。

| | コメント (0) | トラックバック (0)

2004.09.18

テキストのフォーマット(3.戦略の呼び出し)

前々回の「テキストのフォーマット(1.戦略の作成)」と前回の「テキストのフォーマット(2.戦略の登録)」で,フォーマット機能の追加は完了した。しかし,このままではフォーマット処理を呼び出すことができず,宝の持ち腐れになってしまう。つまり,ユーザがフォーマット機能を呼び出すことができるように,UIを準備しなければならない。ただし,テキストエディタに対してフォーマット機能の実行を呼び出すためには,ちょっと工夫が必要である。エディタクラスのformatメソッドを呼ぶだけ,なんて簡単なものではない。

テキストのコピーやペースト,アンドゥや文字列の選択などのテキストエディタに対する操作は,ITextOperationTargetインタフェースを通して行う。つまり,テキストエディタに対する指令は,ITextOperationTargetインタフェースに規定されているdoOperationメソッドを使用するように統一されているのだ。フォーマット機能の実行についてもITextOperationTargetインタフェースを通じて行う。ITextOperationTargetオブジェクトの取得については,テキストエディタを規定しているITextEditorインタフェースのgetAdapterメソッドを使うことによって取得することができる

  IEditorPart editorPart = ...;

  ITextEditor textEditor = (ITextEditor)editorPart;
  ITextOperationTarget target =
    (ITextOperationTarget)textEditor.getAdapter(ITextOperationTarget.class);
  target.doOperation(ISourceViewer.FORMAT);

フォーマット機能を実行するための定数として,ISourceViewerインタフェースにFORMAT定数が定義されている。つまり,ITextOperationTargetオブジェクトのdoOperationメソッドにISourceViewer.FORMATを渡すことにより,フォーマット機能が実行される。結果として,テキストエディタの内容は以下の図のように変更される。

format2.gif

上記のフォーマット機能の呼び出し処理は,エディタのコンテキストメニューのアクションクラスなどに記述して使用すると良いだろう。

以上がフォーマット機能の実装方法である。実際の整形処理については開発者自身が記述することなので,腕の見せ所といったところだろうか。

| | コメント (0) | トラックバック (0)

テキストのフォーマット(2.戦略の登録)

さて,前回の「テキストのフォーマット(1.戦略の作成)」では,各区画用にフォーマット処理クラスを作成した。しかし,テキストエディタが直接これらのオブジェクトを利用するわけではない。ではどうなっているかというと,JFaceが提供しているテキストエディタフレームワークは,フォーマット処理をIContentFormatterオブジェクトに依頼するIContentFormatterインタフェースには,指定されたコンテントタイプに対応するIFormattingStrategyオブジェクトを返すgetFormattingStrategyメソッドが規定されている。つまり,フレームワークからフォーマット処理を依頼されたIContentFormatterオブジェクトは,それに対して登録されたIFormattingStrategyオブジェクトをコンテントタイプに応じて利用することによりフォーマット処理を遂行していくのである。

IFormattingStrategyインタフェースの実装クラスは前回自作したが,IContentFormatterインタフェースに関してはEclipseがデフォルト実装クラスとしてContentFormatterクラスを提供してくれている。通常はContentFormatterクラスを使用すればよい。

IContentFormatterオブジェクトをエディタに提供する処理は,SourceViewerConfigurationクラスのサブクラスで行う。SourceViewerConfigurationクラスのサブクラスにてgetContentFormatterメソッドをオーバーライドし,その中でContentFormatterオブジェクトを生成して返却するようにする。

  public class MySourceViewerConfiguration extends SourceViewerConfiguration {
    ...
    public IContentFormatter getContentFormatter(ISourceViewer sourceViewer) {
      ContentFormatter formatter = new ContentFormatter();
      formatter.setFormattingStrategy(
        new MetaPartFormattingStrategy(), "META_PART");
      formatter.setFormattingStrategy(
        new ContentPartFormattingStrategy(), "CONTENT_PART");
      return formatter;
    }
  }

ContentFormatterオブジェクトを生成し,setFormattingStrategyメソッドを使ってIFormattingStrategyオブジェクトを登録する。setFormattingStrategyメソッドには,第1引数にIFormattingStrategyオブジェクトを,第2引数に対象とする区画の識別子を指定する。そしてIFormattingStrategyオブジェクトが登録されたContentFormatterオブジェクトをgetContentFormatterメソッドの結果として返却する。もちろん上記のSourceViewerConfigurationクラスのサブクラスのインスタンスをAbstractTextEditorクラスのsetSouceViewerConfigurationメソッドを使って登録することも忘れずに。

以上でフォーマット機能の追加は完了である。しかし,このままではフォーマット処理を呼び出すことができず,宝の持ち腐れになってしまう。次回「テキストのフォーマット(3.戦略の呼び出し)」はフォーマット機能を呼び出すための処理について紹介する。

| | コメント (0) | トラックバック (0)

テキストのフォーマット(1.戦略の作成)

Javaエディタには,インデントや空白の調整を行うフォーマット機能が備わっていて,誰が書いたコードに関しても一定の規律に従ってソースコードを整えることができる。Eclipseでは,さまざまな言語に対応したエディタを自作するための強力なフレームワークが提供されているが,このフォーマット機能もその一つである。今回は自作したエディタに対して,入力された内容を整形するフォーマット機能の作成方法を紹介する。

JFaceが提供しているテキストエディタフレームワークでは,入力されたテキストについて,あるルールに従って複数の区画(パーティション)に分割するのが普通である。例えばJavaエディタでは,Javadocコメントの部分と実際のコードの部分に区画が分けられている。特にプログラム言語を扱う場合,一つのソースコード中でもいくつかの分野(コメント文やマクロ,関数など)が混在しているので,インデントや空白,改行位置などのテキストの整形を行いたい場合,その分野ごとに異なる整形を施したくなる。そこで,テキストエディタフレームワークでは,テキストの区画ごとに異なる整形戦略(Strategy)を割り当てられるようになっている

このフォーマット機能は,具体例を示しながらの方がわかりやすいだろう。ここでは,META_PARTおよびCONTENT_PARTという2つの区画が存在するエディタを題材とする。META_PART区画は{$BEGIN_META}から{$END_META}で囲まれた部分,CONTENT_PART区画は{$BEGIN_CONTENT}から{$END_CONTENT}で囲まれた部分とし,それぞれ異なる背景色を定義してある。入力した状況を下図に示す。

format1.gif

META_PART区画については英小文字をすべて英大文字に変更,CONTENT_PART区画については句読点「。」で改行するようにフォーマット機能を仕上げる。

では,フォーマット処理を行うクラスの作成方法から紹介する。フォーマット処理はIFormattingStrategyインタフェースに規定されている。基本的には,IFormattingStrategyインタフェースの実装クラスでformatメソッドに整形処理を実装すればよい

  public class MetaPartFormattingStrategy implements IFormattingStrategy {
    public void formatterStarts(String initialIndentation) {}
    public String format(String content, boolean isLineStart, String indentation, int[] positions) {
      return content.toUpperCase();
    }
    public void formatterStops() {}
  }

フォーマット処理の直前にformatterStartsメソッドが,フォーマット処理の直後にformatterStopsメソッドがそれぞれ呼び出される。この際,formatterStartsメソッドの引数にフォーマット処理対象の文字列の中で最初の行に使われているインデント文字列(タブあるいは1つ以上の空白)が渡される。

そして実際のフォーマット処理を記述するのがformatメソッドである。基本的に,引数のcontent変数にフォーマット対象の文字列が渡されてくるので,それに対してフォーマット処理を行い,その結果の文字列を返却すればよい。フォーマット処理の呼び出し時点で,もし区画内で文字列が選択されていない場合は区画全体の文字列が,もし区画内で文字列が選択されていた場合は選択文字列がcontent引数に渡されてくる。上記の例では,渡された文字列に対してtoUpperCaseメソッドを使用して英小文字を英大文字に変換し,その結果の文字列をフォーマット処理後の文字列として返却している。

その他の引数として,isLineStart引数は処理対象の文字列の最初の行が0桁目から開始されているかどうかが,indentation引数は処理対象の文字列の最初の行に使用されているインデント文字列が渡される。最後のpositions引数は,Javadocには更新される文字の位置と記述されているが,よくわからず(誰か教えてください)。

上記のコードはMETA_CONTENT区画に対してのフォーマット処理クラスである。CONTENT_PART区画に対してのフォーマット処理クラスについても紹介しておこう。

  public class ContentPartFormattingStrategy implements IFormattingStrategy {
    public void formatterStarts(String initialIndentation) {}
    public String format(String content, boolean isLineStart, String indentation, int[] positions) {
      String temp = "", result = "";
      StringTokenizer st = new StringTokenizer(content, "\r\n");
      while(st.hasMoreTokens())
        temp += st.nextToken();
      BreakIterator iterator = BreakIterator.getSentenceInstance();
      iterator.setText(temp);
      int start = iterator.first();
      for (int end = iterator.next(); end != BreakIterator.DONE; start = end, end = iterator.next())
        result += (start == 0) ? temp.substring(start, end) : "\r\n" + temp.substring(start, end);
      if (result.startsWith("{$BEGIN_CONTENT}"))
        result = result.substring(0, 16) + "\r\n" + result.substring(16);
      return result;
    }
    public void formatterStops() {}
  }

BreakIteratorクラスを使って句読点を区切りとして文字列を分割し,その間に改行を付与することによって整形処理を行っている。

Eclipseでは,大抵のインタフェースに対応して提供されているデフォルト実装クラスを使えば,プラグイン開発者は自分で実装クラスを作成しなくても済んでしまう場面が多い。しかし,IFormattingStrategyインタフェースについては,デフォルト実装クラスは特に用意されていない(JDTにはもちろん実装が存在する)。汎用的なフォーマット処理なんぞ存在しない,ということだろう。しかし,ブロックの開始文字と終了文字を与えることによってインデント整形をやってくれる汎用実装クラスくらいは用意されてても良い気がする。。。

さて,これで各区画に対して行うフォーマット処理を実装することができた。これを使用可能にするためには,あといくつかのステップを踏む必要がある。次回「テキストのフォーマット(2.戦略の登録)」は,IFormattingStrategyオブジェクトをフレームワークに登録する方法を紹介する。

| | コメント (0) | トラックバック (0)

2004.09.12

テキストエディタのコンテキストメニューID

ビューやエディタのコンテキストメニュー(右クリックで表示されるメニュー)にアクションを追加するときに,そのコンテキストメニューのIDを知る必要がある。ビューに関しては「特定ビューへのコンテキストメニュー項目の追加」で既に取り上げている。ではエディタの場合はどうなのか,ということで今回の話題としてみよう。

Eclipseの多くのテキストエディタはAbstractTextEditorクラスを継承しているが,テキストエディタのコンテキストメニューのIDはAbstractTextEditorクラスで決定される(もちろんAbstractTextEditorクラスを継承していないエディタは対象外)。特に何も指定がなければ,テキストエディタのコンテキストメニューのIDは,

  エディタID.EditorContext

となる。例えば「yoichiro.eclipse.editor.HogeEditor」というIDのエディタがあったとしたら,そのエディタのコンテキストメニューのIDは「yoichiro.eclipse.editor.HogeEditor.EditorContext」となる。また,Eclipseに標準で搭載されるテキストエディタとJavaエディタに関しては,

  ・デフォルトテキストエディタ(TextEditorクラス)
    org.eclipse.ui.DefaultTextEditor.EditorContext

  ・Javaエディタ(CompilationUnitEditorクラス)
    org.eclipse.jdt.ui.CompilationUnitEditor.EditorContext

となる。何かのテキストエディタのコンテキストメニューに対してアクションを追加したければ,対象のエディタのIDを知ることができれば実現できる。

さて,上記とは別に,Javaエディタのコンテキストメニューにアクションを追加する際,コンテキストメニューのIDに#CompilationUnitEditorContextという文字列を指定している記述をよく見受けると思う。本ブログでも「Javaエディタのコンテキストメニュー」にて#CompilationUnitEditorContextについて紹介している。Javaエディタの場合は,上記の「org.eclipse.jdt.ui.CompilationUnitEditor.EditorContext」でもいいし,「#CompilationUnitEditorContext」でも良い。さて,#CompilationUnitEditorContextはどこから来たのだろうか?

AbstractTextEditorクラスでは,コンテキストメニューのIDをサブクラスにて自由に決めることができる。AbstractTextEditorクラスのsetEditorContextMenuIdメソッドにコンテキストメニューとしたい文字列を指定することで,「エディタID.EditorContext」とは別にコンテキストメニューが設定される

上記で紹介したデフォルトテキストエディタとJavaエディタでは,setEditorContextMenuIdメソッドを使って独自にコンテキストメニューのIDを設定している

  ・デフォルトテキストエディタ(TextEditorクラス)
    #TextEditorContext
      (TextEditorクラスのinitializeEditorメソッド内で設定)

  ・Javaエディタ(CompilationUnitEditorクラス)
    #CompilationUnitEditorContext
      (CompilationUnitEditorクラスのコンストラクタ内)

setEditorContextMenuIdメソッドを使って設定されたコンテキストメニューのIDは,Javadocに記載されるなりしないと,ソースを見ることなしに知ることはできない。TextEditorクラスのAPIリファレンスにはちゃんと記載されているが,Javaエディタに関してはCompilationUnitEditorクラスがinternalなこともあり,特にドキュメントに記載がない。

AbstractTextEditorクラスを継承してテキストエディタを自作して,setEditorContextMenuIdメソッドを使って独自にコンテキストメニューのIDを設定したときは,ちゃんとAPIリファレンスなどにコンテキストメニューのIDを記載すると,開発者に優しいエディタになるだろう。

| | コメント (0) | トラックバック (0)

2004.09.09

MarkerUtilitiesクラス

リソースへのマーキング」で,マーカーの作成方法を紹介した。これに関して,例えば,

  IMarker marker = ...;
  marker.setAttribute(IMarker.CHAR_START, new Integer(20));
  marker.setAttribute(IMarker.CHAR_END, new Integer(25));

としてしまうと,setAttributeメソッドの呼び出しのたびにリソースの変更を監視しているリスナーに通知が行われてしまう(上記では2回連続して呼び出されてしまう)ので,あまりよろしくない。その代わりに,

  Map map = new HashMap();
  map.put(IMarker.CHAR_START, new Integer(20));
  map.put(IMarker.CHAR_END, new Integer(25));
  marker.setAttributes(map);

というようにして,属性の設定時には通知が一回で済むようにしましょう,ということを「リソースへのマーキング」で取り上げた。

しかし,マーカーの作成は,マーカー自身の作成と属性の設定が同時に行われることがほとんどである。マーカーを作成した時点で,リソースの変更を監視しているリスナーに通知が行われてしまうために,

  IResource resource = ...;
  IMarker marker = resource.createMarker(IMarker.TASK);
  Map map = ...;
  marker.setAttributes(map);

としてしまうと,リスナーへの通知が2度行われてしまう。たかがリスナーへの通知が2回行われるだけだと簡単に思うなかれ,処理の内容的に問題がなくても速度的な問題が発生してしまう可能性は十分にある。

では,それを避けるためにはどうすればいいかというと,Eclipseでは便利なMarkerUtilitiesクラスを提供してくれている。MarkerUtilitiesクラスのcreateMarkerメソッドを使用することで,マーカーの作成および属性のセットを一まとめにすることができ,リスナーへの通知も1回のみになる

  IResource resource = ...;
  Map map = new HashMap();
  MarkerUtilities.setCharStart(map, 20);
  MarkerUtilities.setCharEnd(map, 25);
  MarkerUtiltites.createMarker(resource, map, IMarker.TEXT);

属性を格納するコレクションは自分で確保するのだが,コレクションへの属性値の追加は「標準マーカーの属性」で紹介したものについては便利メソッドが提供されている(上記コード中のsetCharStartメソッドなど)。リソースオブジェクト,属性が格納されたコレクション,そしてマーカーの種別を表すID文字列をMarkerUtilitiesクラスのcreateMarkerメソッドに渡すことでマーカーが作成され,その結果のリソースの変更通知は1回で抑えられる。

ここで注意すべき点は,MarkerUtilitiesクラスのcreateMarkerメソッドは,作成したIMarkerオブジェクトを返却してくれないということだ。例えば「テキストエディタで指定位置へジャンプ」をしようとした際,マーカーをMarkerUtilitiesクラスを使って作成してもIMarkerオブジェクトを得ることができないために,gotoMarkerメソッドが使えない。その場合は複数回のリスナーへの通知を抑えることはあきらめるか,MarkerUtilities#createMarker()の内容を自分で記述するしかない。ちょっと残念である。

| | コメント (0) | トラックバック (0)

2004.09.08

テキストエディタで指定位置へジャンプ

テキストエディタ上で「Ctrl+L」キーを押すと出てくる指定行ジャンプ機能。これをプラグインのプログラムから行う方法を紹介する。

テキストエディタ上のある地点(位置または行)に移動させるには,まず移動したい場所にマーカーをつける。そして,そのマーカーめがけてジャンプする。以下が,そのコードの例である。

  IEditorPart editorPart = ...;

  IEditorPart editorPart = editorPart.getEditorInput();
  IResource resource = (IResource)editorPart.getAdapter(IResource.class);

  Map attributes = new HashMap();
  attributes.put(IMarker.CHAR_START, new Integer(50));
  attributes.put(IMarker.CHAR_END, new Integer(50));
  IMarker marker = resource.createMarker(IMarker.TEXT));
  marker.setAttributes(attributes);

  IDE.gotoMarker(editorPart, marker);

  marker.delete();

処理対象のIEditorPartオブジェクトからIEditorInputオブジェクトを取得し,IEditorInputオブジェクトから実際のリソース(IResource)オブジェクトをgetAdapterメソッドを使って取得する。通常はIFileオブジェクトが返却されるだろう。

IResourceオブジェクトが取得できたら,「リソースへのマーキング」で紹介したcreateMarkerメソッドを使って,マーカーを作成する。この際,マーカーの種類として「標準マーカー」で紹介したIMarker.TEXTを指定する。そしてマーカーの属性として,IMarker.CHAR_STARTおよびIMarker.CHAR_ENDにエディタ上でジャンプしたい位置を指定する。各属性値に同じ値を指定することで,エディタ内の文字列の先頭文字からの指定位置にカーソルが移動される。異なる値を指定したときは,その範囲の文字列が選択されて,カーソルが選択文字列の直後に位置される。

マーカーが作成できたら,IDEクラスのgotoMarkerメソッドにIEditorPartオブジェクトとIMarkerオブジェクトを渡すことで,指定された場所にジャンプされる。gotoMarkerメソッドの実行後は,マーカーは必要ないものになるので,deleteメソッドを使用してマーカーを削除しておく

上記は指定位置へのジャンプだったが,指定行へのジャンプの場合は,マーカーの属性としてIMarker.LINE_NUMBERを使用する

  Map attributes = new HashMap();
  attributes.put(IMarker.LINE_NUMBER, new Integer(5));

これにより,5行目の文字列が選択され,6行目の先頭にカーソルが移動されるようになる。

実は上記のジャンプ処理は,AbstractDecoratedTextEditorクラスを継承しているテキストエディタに関して適用される動きであり,エディタであれば何でもそうなるわけではない。EclipseのデフォルトテキストエディタであるTextEditorクラスや,Javaソースコード編集用のエディタであるCompilationUnitEditorクラスは,このAbstractDecoratedTextEditorクラスを継承しているので,上記の方法が使用できる。

マーカーへのジャンプ処理は,IGotoMarkerインタフェースによって規定されている。IDEクラスのgotoMarkerメソッドは,指定されたIEditorPartオブジェクトがIGotoMarkerインタフェースを実装しているかをチェックし,実装していればIEditorPartオブジェクトのgotoMarkerメソッドを呼び出して,ジャンプ処理を行わせている。つまり,エディタがIGotoMarkerインタフェースのgotoMarkerメソッドをどのように実装しているかによって,ジャンプ処理の動きが変わってくるということになる。

| | コメント (0) | トラックバック (0)

2004.09.03

エディタを開く Part2

もう3ヶ月近く前になるが,2004年5月24日に「エディタを開く」という記事を書いた。あるファイルをプラグインのプログラム内からエディタを使って開くための方法を紹介したのだが,当時はEclipseのバージョン2.1.3を対象にしていた。6月に入ってEclipseのバージョン3.0が公開されたわけだが,実はEclipse3.0になってからいくつかのAPIが廃止あるいは非互換になっている。どんなAPIが対象化というと,Eclipseのヘルプの中の,

  「Platform Plug-in Developer Guide」→「3.0 Plug-in Migration Guide」→「Incompatibilities」

を見れば非互換になったものを把握することができる。これを見てみると,「エディタを開く」で紹介したIWorkbenchPage#openEditor(IFile)メソッドが,見事に廃止の対象になっていた。残念ながら,完全に「エディタを開く」はEclipse3.0以降では参考にならない記事になってしまったので,ここでエディタを開くための新しい方法を紹介する。

Eclipse3.0からは,IWorkbenchPageインタフェースのopenEditorメソッドについて,引数にIFileオブジェクトを取るものが廃止され,IEditorInputオブジェクトおよびエディタIDの文字列を引数に取るメソッドのみに限定された。その理由は「IWorkbenchPartインタフェースは,一般的なワークベンチの中での定義なのにも関わらず,IFileなど特定のリソースに依存してしまっているので,それに関連するメソッドを削除した」ということらしい。ごもっともである。

さて,ではIFileオブジェクトを使って直接エディタを開くことはできなくなってしまったかというと,そうではない。Eclipse3.0からIDEクラスが追加され,IDEクラスにIFileオブジェクトを使ってエディタを開くためのメソッドが提供されている。

  IFile file = ...;

  IWorkbench workbench = PlatformUI.getWorkbench();
  IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
  IWorkbenchPage page = window.getActivePage();

  IEditorPart editorPart = IDE.openEditor(page, file);

アクティブなIWorkbenchPartオブジェクトを取得するのは「エディタを開く」と同じ。IWorkbenchPartオブジェクトが取得できれば,あとはIDEクラスのクラスメソッドであるopenEditorメソッドに,IWorkbenchPartオブジェクトとIFileオブジェクトを渡せば,エディタが開かれ,その結果のIEditorPartオブジェクトが返却される

このIDEクラスのopenEditorメソッド,結局はIWorkbenchPageインタフェースのopenEditorメソッドが利用されている。つまり,IFileオブジェクトを持つIFileEditorInputオブジェクトを生成し,さらにIFileオブジェクトで表されるファイルの名前から開くべきエディタの種類(エディタID)を決定し,それらをIWorkbenchPageインタフェースのopenEditorメソッドに渡してエディタを開く,という処理である。どのエディタで開くか(エディタIDの取得)は,IDEクラスのgetEditorDescriptorメソッドに判断ロジックが記述されている。その判断ロジックは,「エディタを開く」で紹介したものとほぼ同じである。

このようなインタフェース間やクラス間の依存性の排除などの見直しがされているあたりに,Eclipseの設計者のこだわりというか理想の高さを感じる。

| | コメント (0) | トラックバック (0)

« 2004年8月 | トップページ | 2004年10月 »