Eclipseのプラグイン開発の自作DMDLエディターでフォールディングできるようにしてみる。
|
フォールディングとは、ソースの折りたたみ機能のことらしい。
EclipseのJavaエディターだと、エディターの左側にある「+」アイコンでブロックを閉じたり開いたりできる。それのこと。
作り方は『Eclipse プラグイン開発 徹底攻略』p.182にある通り。
ドキュメント(エディター上のテキスト)の解釈処理を作る以外は、本の通りで問題ない。
フォールディングを行う為には、ドキュメント(エディター上のテキスト)を構文解析して、フォールディング範囲を決定する必要がある。
DMDLエディターの場合は、データモデルのブロックである波括弧で囲まれた部分と、(Javaエディターに似せて)トップレベルのブロックコメントをフォールディング範囲としよう。
構文解析には、テキストの色を付ける為のRuleクラス(自作したDMDLPartitionRule)を利用することにする。
import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.projection.ProjectionAnnotation; import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
class ApplyRule extends DMDLPartitionRule { /** コンストラクター. */ public ApplyRule() { super(null); }
@Override protected void readBlock(ICharacterScanner scanner) { Scanner s = (Scanner) scanner; int start = s.getOffset(); super.readBlock(scanner); int end = s.getOffset(); apply(s.document, start, end, s.model); }
readBlock()は波括弧で囲まれたブロックを解釈するメソッド。
解釈本体(親クラスのメソッド)を呼ぶ前後でその時点のドキュメントの解釈位置を保持しておき、それをフォールディング範囲にしている。
@Override protected void readToCommentEnd(ICharacterScanner scanner, boolean top) { if (top) { Scanner s = (Scanner) scanner; int start = s.getOffset(); super.readToCommentEnd(scanner, top); int end = s.getOffset(); apply(s.document, start, end, s.model); } else { super.readToCommentEnd(scanner, top); } }
readToCommentEnd()はブロックコメント(/*〜*/)を解釈するメソッド。
トップレベル(ブロック等の中に入っていないコメント)の場合だけフォールディング範囲にしている。
void apply(IDocument document, int start, int end, ProjectionAnnotationModel model) { try { int startLine = document.getLineOfOffset(start); int endLine = document.getLineOfOffset(end); if (startLine != endLine) { int len = end - start; Position pos = new Position(start, len); model.addAnnotation(new ProjectionAnnotation(), pos); } } catch (BadLocationException e) { } } }
ProjectionAnnotationModelにフォールディング範囲を登録していく。
Postionに指定するフォールディング範囲の長さ(len)は、行末の改行コードの位置まで含めないと、
エディター上で閉じた際に最終行が閉じられずに残ってしまう。(今回はその辺りの厳密さは無視)
ルールの解釈を実行する為にはScannerが必要。
色を付けるのに使っているScannerだと今回のケースでは使いづらい(色々な機能が付いていて動作が予測しづらい)ので、独自に作成する。
class Scanner implements ICharacterScanner { private IDocument document; private int offset; private int length; private int pos; private ProjectionAnnotationModel model; public Scanner(IDocument document, int offset, int length, ProjectionAnnotationModel model) { this.document = document; this.offset = offset; this.length = length; pos = 0; this.model = model; }
@Override public char[][] getLegalLineDelimiters() { throw new UnsupportedOperationException(); } @Override public int getColumn() { throw new UnsupportedOperationException(); }
@Override public int read() { if (pos >= length) { return EOF; } int n = offset + pos; pos++; try { return document.getChar(n); } catch (BadLocationException e) { pos--; return EOF; } }
@Override public void unread() { pos--; }
public int getOffset() { return offset + pos - 1; } }
DMDLEditorでフォールディングの設定を行う。
import org.eclipse.jface.text.source.projection.ProjectionSupport; import org.eclipse.jface.text.source.projection.ProjectionViewer;
public class DMDLEditor extends TextEditor implements IPropertyChangeListener {
〜
protected ProjectionSupport projectionSupport;
@Override protected ISourceViewer createSourceViewer(Composite parent, IVerticalRuler ruler, int styles) { // フォールディングの為のViewerを作成 IOverviewRuler overviewRuler = getOverviewRuler(); boolean overviewRulerVisible = isOverviewRulerVisible(); ISourceViewer viewer = new ProjectionViewer(parent, ruler, overviewRuler, overviewRulerVisible, styles); getSourceViewerDecorationSupport(viewer); return viewer; }
デフォルトのcreateSourceViewer()ではISourceViewerとしてSourceViewerインスタンスを作っているが、フォールディングにはProjectionViewerを使う。
(getOverviewRuler()は概説ルーラーを生成する。これをProjectionViewerに指定しないと、概説ルーラーが表示されない。[2013-06-05])
getOverviewRuler()と同じものがfOverviewRulerフィールドに保持されるが、createSourceViewer()の時点ではnullなので、fOverviewRulerをProjectionViewerに渡すと概説ルーラーは表示されない。getOverviewRuler()を呼び出してOverviewRulerを初期化する必要がある。
(getOverviewRuler()はfOverviewRulerがnullだとcreateOverviewRuler()を呼び出してfOverviewRulerにセットする仕組みになっている)
@Override public void createPartControl(Composite parent) { super.createPartControl(parent); { // フォールディングの設定 ProjectionViewer viewer = (ProjectionViewer) getSourceViewer(); projectionSupport = new ProjectionSupport(viewer, getAnnotationAccess(), getSharedColors()); projectionSupport.install(); viewer.doOperation(ProjectionViewer.TOGGLE); updateFolding(); //フォールディング範囲算出(初回) } { // 対応する括弧の強調表示の設定 〜 } }
@Override
public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
if (projectionSupport != null) {
Object obj = projectionSupport.getAdapter(getSourceViewer(), adapter);
if (obj != null) {
return obj;
}
}
return super.getAdapter(adapter);
}
@Override public void doSaveAs() { super.doSaveAs(); updateFolding(); //フォールディング範囲の再計算 } @Override public void doSave(IProgressMonitor progressMonitor) { super.doSave(progressMonitor); updateFolding(); //フォールディング範囲の再計算 }
エディター上で文字を打つ度に自動的にフォールディング範囲が変わるわけではないので、
とりあえず、ファイル保存時にフォールディング範囲を再計算するようにしておく。
/** * フォールディング範囲を最新状態に更新する. */ private void updateFolding() { try { ProjectionViewer viewer = (ProjectionViewer) getSourceViewer(); if (viewer == null) { return; } ProjectionAnnotationModel model = viewer.getProjectionAnnotationModel(); if (model == null) { return; } // 全てのフォールディング範囲をクリア model.removeAllAnnotations(); // ドキュメントのパーティションを走査してフォールディング範囲を決定 IDocument document = getDocumentProvider().getDocument(getEditorInput()); for (int offset = 0; offset < document.getLength();) { ITypedRegion part = document.getPartition(offset); String type = part.getType(); if (DMDLPartitionScanner.DMDL_BLOCK.equals(type)) { applyFolding(document, part, model); } offset += part.getLength(); } } catch (Exception e) { ILog log = Activator.getDefault().getLog(); log.log(new Status(Status.WARNING, Activator.PLUGIN_ID, "updateFolding error.", e)); } }
private ApplyRule rule = new ApplyRule(); protected void applyFolding(IDocument document, ITypedRegion part, ProjectionAnnotationModel model) throws BadLocationException { int offset = part.getOffset(); int length = part.getLength(); Scanner scanner = new Scanner(document, offset, length, model); for (;;) { IToken t = rule.evaluate(scanner); if (t == Token.EOF) { break; } if (t == Token.UNDEFINED) { scanner.read(); } } }
よく考えたら、ドキュメントを毎回全走査しているんだから、(文字を入力する度に狂う可能性のある)パーティションを使わず、
直接ドキュメントを頭から走査すればいいんじゃん(爆)
/**
* フォールディング範囲を最新状態に更新する.
*/
private void updateFolding() {
〜
// 全てのフォールディング範囲をクリア
model.removeAllAnnotations();
// ドキュメントを走査してフォールディング範囲を決定
applyFolding(document, model);
} catch (Exception e) {
ILog log = Activator.getDefault().getLog();
log.log(new Status(Status.WARNING, Activator.PLUGIN_ID, "updateFolding error.", e));
}
}
protected void applyFolding(IDocument document, ProjectionAnnotationModel model) throws BadLocationException {
int offset = 0;
int length = document.getLength();
Scanner scanner = new Scanner(document, offset, length, model);
〜
}