« 2004年7月 | トップページ | 2004年9月 »

2004.08.27

テキストエディタ上のカーソル位置の取得方法

テキストエディタ上である機能が呼び出された時点でのカーソル(キャレット)位置の取得を行いたいときがある。カーソル位置から現在行を取得したり,カーソル位置がメソッド内かどうかを調べたり,などが例として考えられる。カーソル位置の取得なんて簡単にできそうな気がするが,実はそこには高い壁が存在している

AbstractTextEditorクラスを基底クラスとするテキストエディタは,ViewのコンポーネントとしてSWTのStyledTextクラスを利用している。このStyledTextクラスのgetCaretOffsetメソッドを呼び出すことで,カーソル位置を取得することができる。「なんだ,それならStyleTextオブジェクトを取得できればいいだけだから簡単じゃん」と思ってしまうが,ここで落とし穴が待っている。

StyledTextオブジェクトを取得するためには,ISourceViewerインタフェース(実際の定義は親のITextViewerインタフェース)のgetTextWidgetメソッドを使用すれば良いのだが,ISourceViewerオブジェクトを取得するためのAbstractTextEditorクラスのgetSourceViewerメソッドのアクセス修飾子がprotectedなのである。つまり,テキストエディタが持つStyledTextオブジェクトの取得は,AbstractTextEditorクラスのサブクラスからのみに限定されていることになる。これでは,Eclipse既存のテキストエディタ(TextEditorやJDTのCompilationUnitEditorなど)の外部(アクション代理オブジェクトなど)からはStyledTextオブジェクトを得ることができない。結果として,AbstractTextEditorクラスのサブクラスを作ってテキストエディタを自作しない限り,カーソル位置を取得することができないことになる。

もちろんAbstractTextEditorクラスのサブクラスを自作すれば,カーソル位置を取得することが可能になる。その方法は以下のようなコードになる。

  ISourceViewer sourceViewer = getSourceViewer();
  StyledText styledText = sourceViewer.getTextWidget();
  int widgetOffset = styledText.getCaretOffset();
  int modelOffset = widgetOffset2ModelOffset(sourceViewer, widgetOffset);

getSourceViewerメソッドを使ってISourceViewerオブジェクトを取得,その後ISourceViewerオブジェクトのgetTextWidgetメソッドを呼び出してStyledTextオブジェクトを取得する。そしてStyledTextオブジェクトのgetCaretOffsetメソッドを使って,カーソル位置を取得する。ただし,getCaretOffsetメソッドで取得したカーソル位置は,あくまで画面上の見かけの位置であり,テキスト全体から見た位置ではない。Eclipseのバージョン3から搭載されたFolding機能によってテキストのある部分が非表示になることがあるため,StyledTextオブジェクトのgetCaretOffsetメソッドの結果はFolding機能によって非表示になったテキストがすっ飛ばされた位置なのである。IDocumentインタフェースで表されるテキストのモデル(つまりテキスト全体)からみた現在のカーソル位置を得るために,widgetOffset2ModelOffsetメソッドが用意されている。widgetOffset2ModelOffsetメソッドにStyledTextオブジェクトから得たカーソル位置を渡すことにより,テキスト全体から見た位置を計算して返却してくれるので,その結果をカーソル位置として使用すればよい。

さて,上記の方法はもちろんAbstractTextEditorクラスのサブクラスからしか利用できない。では方法がないのかというとそんなことはなくて,テキストの選択情報を取得するための機構を流用することで(なんちゃってではあるが)カーソル位置の取得を実現できる。テキストが選択されていない状態で選択状況の取得を行うと,選択開始位置から長さ0の選択,という情報が得られるので,その選択開始位置をカーソル位置とすることができる。もちろんこの位置はテキスト全体から見たモデル上の位置である。

  IEditorPart editorPart = ...;

  ITextEditor textEditor = (ITextEditor)editorPart;
  ISelectionProvider selectionProvider = textEditor.getSelectionProvider();
  ISelection selection = selectionProvider.getSelection();
  ITextSelection textSelection = (ITextSelection)selection;
  int offset = textSelection.getOffset();

まず,エディタを表すIEditorPartオブジェクトをITextEditor型にキャストする。そしてITextEditorオブジェクトのgetSelectionProviderメソッドを使ってISelectionProviderオブジェクトを取得する。さらに,ISelectionProviderオブジェクトのgetSelectionメソッドを使用してISelectionオブジェクトを取得し,それをITextSelection型にキャストする。ITextSelectionオブジェクトには選択の開始位置および選択文字数が格納されているので,getOffsetメソッドを使って選択開始位置を取得する。結果として,この選択開始位置をカーソル位置とする。

ただし,この方法では完全にはカーソル位置を取得することができない。エディタ上で文字列の選択を[Shift]+[→]キーで行った場合,選択後のカーソルの位置は選択文字列の最後に位置する。つまり,カーソル位置は本当は「textSelection.getOffset() + textSelection.getLength()」なのだが,選択行為を[→]キーで行ったのか[←]キーで行ったのかを得る手段がないために,選択開始位置にカーソルがあるのか選択終了位置にカーソルがあるのか区別がつかない。よって,上記のように選択開始位置をカーソル位置とする,という固定的な方法にするしかないのである。

ワープロじゃなくてテキストエディタなんだから,カーソルの現在位置の取得メソッドくらい,publicで用意されていてもいい気がするけど,何か理由があるのだろうか。。。

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

2004.08.24

エディタからICompilationUnitオブジェクトを取得する方法

JDTに付属されているJavaのソースコードを編集するためのエディタに対して機能拡張を行う際に,そのエディタで編集されているソースコードのJavaモデル表現であるICompilationUnitオブジェクトを取得したいことが多々ある。今回は,エディタからこのICompilationUnitオブジェクトを取得するための方法を紹介する。

エディタからICompilationUnitオブジェクトを得る方法は2つ存在する。

(1) ファイルリソースからICompilationUnitオブジェクトを生成する
(2) エディタ入力に実装されているIAdaptableインタフェースを利用する

まずは(1)から紹介する。これは,JDTで提供されているJavaCoreクラスのcreateCompilationUnitFromメソッドを利用する方法である。

  IEditorPart editorPart = ...;

  IFileEditorInput fileEditorInput = (IFileEditorInput)editorPart.getEditorInput();
  IFile file = fileEditorInput.getFile();
  ICompilationUnit iCompilationUnit = JavaCore.createCompilationUnitFrom(file);

最初に,エディタを表すIEditorPartオブジェクトのgetEditorInputメソッドを呼び出して,IEditorInputオブジェクトを取得する。そして,取得したIEditorInputオブジェクトをIFileEditorInput型でキャストする。もちろん本来はキャストできるかどうかチェックする必要があるが,上記では省略している。そしてIFileEditorInputオブジェクトのgetFileメソッドを呼び出して,IFileオブジェクトを取得する。あとは,JavaCoreクラスのクラスメソッドであるcreateCompilationUnitFromメソッドにIFileオブジェクトを渡すことで,ICompilationUnitオブジェクトを得ることができる。

次の(2)の方法は,知っていないと思いもつかない方法である。

  IEditorPart editorPart = ...;

  IEditorInput editorInput = editorPart.getEditorInput();
  ICompilationUnit iCompilationUnit =
    (ICompilationUnit)editorInput.getAdapter(IJavaElement.class);

まず,IEditorPartオブジェクトからIEditorInputオブジェクトを取得する。そして,IEditorInputオブジェクトのgetAdapterメソッドにJavaモデルの要素であることを示すIJavaElementインタフェースのクラスオブジェクトを渡すことで,エディタで開いているソースコードのJavaモデル表現であるICompilationUnitオブジェクトが得られる。

実際にソースコードを追ったわけではないのだが,たぶんJavaエディタでソースコードのファイルが開かれる際に,そのIEditorInputオブジェクトとICompilationUnitオブジェクトをAdapterManagerオブジェクトに登録しているのではないかと思われる。

どちらの方法を使っても得られる結果は同じなので,自由に選択してかまわない。ただ,(1)の方法だとIFileEditorInput型にダウンキャストしている点が,(2)の方法に比べてちょっと問題ありかな,という気がする。

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

2004.08.06

文字列の移動,コピーを行うテキスト編集クラス

前回の「ドキュメントの操作を簡単にするテキスト編集クラス」では,ドキュメントのテキストに対して挿入,削除,置換処理を手軽に行うための方法を紹介した。今回は,挿入などよりもちょっと高級な,文字列の移動,コピーをやってくれるテキスト編集クラスを紹介する。

まずは移動から。文字列の移動は,2つのテキスト編集クラスを組み合わせて行う。つまり,文字列の移動を噛み砕くと「○文字目から○文字分を,○文字目に移動する」っていう表現になるが,この前半の「○文字目から○文字分を」を担当するのがMoveSourceEditクラス,後半の「○文字目に移動する」を担当するのがMoveTargetEditクラスである。

早速使い方を見てみよう。例として,「1234567890」という文字列の最初の「123」を5文字目の後に移動する(結果として「4512367890」とする)処理を考えてみる。

  IDocument document = ...; // "1234567890"

  MoveSourceEdit sourceEdit = new MoveSourceEdit(0, 3);
  MoveTargetEdit targetEdit = new MoveTargetEdit(5);
  sourceEdit.setTargetEdit(targetEdit);

  MultiTextEdit multiTextEdit = new MultiTextEdit();
  multiEdit.addChild(sourceEdit);
  multiEdit.addChild(targetEdit);
  multiTextEdit.apply(document); // "4512367890"

まず,移動元の文字列を特定するためのMoveSourceEditオブジェクトを生成する。この際,第1引数に何文字目の文字列なのか,第2引数には何文字分なのかを指定する。上記では「0文字目から3文字分("123")」を指定している。次に,移動先を特定するためのMoveTargetEditオブジェクトを生成する。引数には,何文字目に挿入するかを指定する。移動先の位置は,MoveSourceEditオブジェクトで特定される文字列の長さなどを気にする必要はなく,移動前の文字列における位置でかまわない。上記では5文字目を指定している。

そして,MoveSourceEditオブジェクトとMoveTargetEditオブジェクトを関連付ける。上記では,MoveSourceEditオブジェクトのsetTargetEditメソッドにMoveTargetEditオブジェクトを渡すことで関連付けを行っている。これの代わりに「targetEdit.setSourceEdit(sourceEdit);」というようにしても良い。

あとは前回の「ドキュメントの操作を簡単にするテキスト編集クラス」の時のように,MultiTextEditオブジェクトにMoveSourceEditオブジェクトとMoveTargetEditオブジェクトをaddCihldメソッドを使って登録し,applyメソッドを使ってIDocumentオブジェクトにテキスト編集を適用する。この際注意することは,setTargetEditメソッドまたはsetSourceEditメソッドを使って関連付けをちゃんとしておくことと(これを怠ると例外が発生する),MultiTextEditオブジェクトにMoveSourceEditオブジェクトとMoveTargetEditオブジェクトの両方をちゃんと登録しておくことである(片方だけだと移動処理が中途半端に終わってしまう)。

上記で移動の話は終わり。次にコピーだが,やり方は基本的に移動のときと同じであるが,使用するクラスが違ってくる。コピーの場合は,コピー元の文字列を特定するためにCopySourceEditクラスを,コピー先の位置を特定するためにCopyTargetEditクラスを使用する。

例として「1234567890」の最初の「123」を5文字目の後にコピーして「1234512367890」とする処理を考えると,以下のようなコードで実現できる。

  IDocument document = ...; // "1234567890"

  CopySourceEdit sourceEdit = new CopySourceEdit(0, 3);
  CopyTargetEdit targetEdit = new CopyTargetEdit(5);
  sourceEdit.setTargetEdit(targetEdit);

  MultiTextEdit multiTextEdit = new MultiTextEdit();
  multiEdit.addChild(sourceEdit);
  multiEdit.addChild(targetEdit);
  multiTextEdit.apply(document); // "1234512367890"

良く見ると,移動のときのサンプルとほぼ同じで,使用しているクラスを変えただけである。setSourceEditメソッドとsetTargetEditメソッドの話や,使用時の注意点などもMove~Editクラスのときと同じである。

あるテキストに対して,次々と編集を一気に加えていく処理を行うときは,「ドキュメントの操作を簡単にするテキスト編集クラス」で紹介したInsertEdit,DeleteEdit,ReplaceEditクラスや,今回取り上げたMove~Edit,Copy~Editクラスを組み合わせて実現することで,パニックを起こさずに処理を記述していけることだろう。

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

2004.08.05

ドキュメントの操作を簡単にするテキスト編集クラス

一般的にエディタに対して機能を拡張するプラグインでは,IDocumentインタフェースで表されるドキュメント(テキスト)を操作することが求められる。すなわち,エディタ内のテキストに対して,文字列の挿入や削除,置換などを行わなければならない。もちろん,IDocumentインタフェースのreplaceメソッドを使えば,テキスト操作を全て行うことができるのだが,ちょっと考えただけでも恐ろしくなるほどreplaceメソッドだけではテキスト操作は難しい

例えば,「09012345678」という電話番号があったとして,これに「-」を2つ加えて「090-1234-5678」としたいとすると,単純に考えると「3文字目の後と,7文字目の後に『-』を挿入する」となる。そこで,

  IDocument document = ...; // 09012345678
  document.replace(3, 0, "-");
  document.replace(7, 0, "-");

と実行してみると,結果は「090-123-45678」となってしまう。1回目のreplaceメソッドの実行によって1文字挿入されてしまったために,挿入すべき位置がずれてしまうからである。もちろん挿入した文字数を考慮して2回目の挿入位置を考慮すればいい話だが,もっと複雑なテキスト編集を行う場合は,はっきりいって誰でも頭の中はパニックになるだろう。

そんな大変なテキスト編集を簡単にするために,Eclipseでは便利なTextEditクラス(とそのサブクラス群)を用意してくれているTextEditクラスの機構を使うことで,上記のような操作位置の計算を自動的に行ってくれる

TextEditクラスは抽象クラスであり,開発者が使うのはTextEditクラスのサブクラスである。以下が代表的なTextEditクラスのサブクラスである。

  ・InsertEdit - 指定位置への文字列の挿入
    ex) new InsertEdit(10, "hoge"); // 9文字目に"hoge"を挿入

  ・DeleteEdit - 指定位置から指定長さ分だけ文字列の削除
    ex) new DeleteEdit(10, 5); // 9文字目から5文字分を削除

  ・ReplaceEdit - 指定位置から指定長さ分の文字列の置換
    ex) new RplaceEdit(10, 5, "hoge"); // 9文字目から5文字分を"hoge"に置換

これらのクラスのインスタンスを複数組み合わせることにより,ドキュメントを操作できる。例えば,先の電話番号の例では,InsertEditオブジェクトを2つ生成すればよい。

  InsertEdit edit1 = new InsertEdit(3, "-");
  InsertEdit edit2 = new InsertEdit(7, "-");

2つ目の「-」を挿入するInsertEditオブジェクトの生成では,1つ目の挿入を気にすることなく,7文字目を指定している。

さて,この2つのテキスト編集オブジェクトをドキュメントに対して一気に適用させるために,MultiTextEditクラスを使用する

  MultiTextEdit multiTextEdit = new MultiTextEdit();
  multiTextEdit.addChild(edit1);
  multiTextEdit.addChild(edit2);

MultiTextEditオブジェクトを生成し,addChildメソッドを使って先ほど生成したInsertEditオブジェクトを登録する。もちろんaddChildメソッドにRemoveEditオブジェクトやReplaceオブジェクトを渡すことも可能である。

最後に,applyメソッドにIDocumentオブジェクトを渡すことで,ドキュメントにテキスト編集を適用させる

  multiTextEdit.apply(document);

これにより,各テキスト編集の位置を内部で自動計算しながら,ドキュメントの内容が変更され,めでたく「090-1234-5678」となる。このように,TextEditクラスのサブクラスを用いることによって,元のドキュメントの内容を基準としたテキスト編集を手軽に行うことができる。しかもテキストの編集内容が複雑になればなるほど,TextEditクラスの恩恵を感じることができるはず。お試しあれ。

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

2004.08.02

テキストバッファとエディタ

突然だが,今回はテキストバッファについてもう少し知識を深めることにしよう。

テキストバッファを使ったIDocumentオブジェクトの取得」で紹介したテキストバッファをプラグイン開発者が直接利用する機会は,そう滅多にないかもしれない。やはりエディタに表示されている内容に対しての操作が中心になるだろうし,裏側で自動的にテキストファイルの内容が変更されているというのを,あまり良く思わない人もいるだろう。ただし,Eclipseはこのテキストバッファ抜きで語ることはできなくなった。

org.eclipse.core.filebuffersプラグインは,実はEclipse3.0から提供されたものである。Eclipse3.0になってから,このテキストバッファはEclipse内で大活躍している。つまり,テキストを扱うエディタは,テキストバッファを利用するように新たに書き直されているのだ。そして,あるファイルを複数のエディタで同時に開いたときは,それぞれのエディタはテキストバッファを共有する。つまり,片方のエディタで行った編集作業が,即座にもう1つのエディタにも伝達される,ということがテキストバッファの採用により実現されているのである(バッファ関連のコールバックメソッドはIFileBufferListenerインタフェースに規定されている)。

org.eclipse.core.filebuffersプラグインにより,テキストバッファは一元管理されている。つまり,

  IPath targetFilePath = ...; // 対象のファイルのパス

  ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
  manager.connect(targetFilePath, null);
  ITextFileBuffer buffer = manager.getTextFileBuffer(targetFilePath);

という処理を行った際に,例えば既にtargetFilePathで示されるファイルが何らかのエディタで開かれていた場合は,既にそのエディタがテキストバッファを作成しているので,上記のgetTextFileBufferメソッドは新たにテキストバッファを作成するのではなく,エディタにより作成されたテキストバッファが返される。よって,

  IDocument document = buffer.getDocument();

としてIDocumentオブジェクトを取得し,そのIDocumentオブジェクトに対して文字列の挿入などを行ったとすると,もちろんエディタにも文字列の挿入が反映される。エディタが持つIDocumentオブジェクトと,テキストバッファから取得したIDocumentオブジェクトが,同一のテキストバッファを指し示している証拠である。

ちなみに,

  buffer.commit(null, true);

とした場合は,エディタで保存を行ったのと同じ効果が得られる。もちろん,エディタ上でも「*」マークが消されるので,エディタの内容(すなわちテキストバッファの内容)が保存されたことがわかるだろう。

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

« 2004年7月 | トップページ | 2004年9月 »