S-JIS[2013-01-20/2013-06-05] 変更履歴

EclipseプラグインDMDLエディター(フォールディング)

Eclipseプラグイン開発自作DMDLエディターでフォールディングできるようにしてみる。


概要

フォールディングとは、ソースの折りたたみ機能のことらしい。
EclipseのJavaエディターだと、エディターの左側にある「+」アイコンでブロックを閉じたり開いたりできる。それのこと。

作り方は『Eclipse プラグイン開発 徹底攻略』p.182にある通り。
ドキュメント(エディター上のテキスト)の解釈処理を作る以外は、本の通りで問題ない。


ドキュメント解釈処理

フォールディングを行う為には、ドキュメント(エディター上のテキスト)を構文解析して、フォールディング範囲を決定する必要がある。
DMDLエディターの場合は、データモデルのブロックである波括弧で囲まれた部分と、(Javaエディターに似せて)トップレベルのブロックコメントをフォールディング範囲としよう。


ApplyRule

構文解析には、テキストの色を付ける為の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が必要。
色を付けるのに使っている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.java

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

Eclipseプラグインへ戻る / Eclipseへ戻る / 技術メモへ戻る
メールの送信先:ひしだま