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);
〜
}