S-JIS[2013-08-20/2013-08-28] 変更履歴

Xtextハイライト

Xtextのハイライト(色付け)のメモ。


概要

エディターで、キーワード等に色が付く。

Xtextでの色付けには、字句解析ルールに対して色を付ける「Lexical Highlighting」と、構文解析ルールに従って色を付ける「Semantic Highlighting」の二種類がある。


デフォルトの字句ハイライトの仕組み

Xtextで構文定義を行ってエディターを生成すると、自動的にキーワードに色が付く。
これは、以下のような仕組みになっている。

トークンからIDへのマッピング

DefaultAntlrTokenToAttributeIdMapperによって、字句解析ルール名から色付け用のIDに変換する。

public class DefaultAntlrTokenToAttributeIdMapper extends AbstractAntlrTokenToAttributeIdMapper {
	private static final Pattern QUOTED = Pattern.compile("(?:^'([^']*)'$)|(?:^\"([^\"]*)\")$", Pattern.MULTILINE);

	private static final Pattern PUNCTUATION = Pattern.compile("\\p{Punct}*");
	@Override
	protected String calculateId(String tokenName, int tokenType) {
		if(PUNCTUATION.matcher(tokenName).matches()) {
			return DefaultHighlightingConfiguration.PUNCTUATION_ID;
		}
		if(QUOTED.matcher(tokenName).matches()) {
			return DefaultHighlightingConfiguration.KEYWORD_ID;
		}
		if("RULE_STRING".equals(tokenName)) {
			return DefaultHighlightingConfiguration.STRING_ID;
		}
		if("RULE_INT".equals(tokenName)) {
			return DefaultHighlightingConfiguration.NUMBER_ID;
		}
		if("RULE_ML_COMMENT".equals(tokenName) || "RULE_SL_COMMENT".equals(tokenName)) {
			return DefaultHighlightingConfiguration.COMMENT_ID;
		}
		return DefaultHighlightingConfiguration.DEFAULT_ID;
	}
}

xtextファイル上で「terminal STRING:〜;」で定義されている字句解析ルールは、「RULE_STRING」というトークン名になっている。

単語で定義されている字句解析ルールは、シングルクォーテーションで囲まれた文字列(単語)としてtokenNameに入ってくる。
これは正規表現のQUOTEDで判定している。

また、「%」や「=」「(」「)」といった記号類もシングルクォーテーションで囲まれた文字列として渡ってくるので、QUOTEDの条件に引っかかってしまう。
そのため、QUOTEDよりも先にPUNCTUATIONで判定している。

IDとスタイルの紐付け

DefaultHighlightingConfigurationによって、色付け用のIDに対してスタイル(文字の色や太字にするかどうか等)を割り当てる。

public class DefaultHighlightingConfiguration implements IHighlightingConfiguration {
	public static final String KEYWORD_ID = "keyword";
	public static final String PUNCTUATION_ID = "punctuation";
	public static final String COMMENT_ID = "comment";
	public static final String STRING_ID = "string";
	public static final String NUMBER_ID = "number";
	public static final String DEFAULT_ID = "default";
	public static final String INVALID_TOKEN_ID = "error";
	public void configure(IHighlightingConfigurationAcceptor acceptor) {
		acceptor.acceptDefaultHighlighting(KEYWORD_ID, "Keyword", keywordTextStyle());
		acceptor.acceptDefaultHighlighting(PUNCTUATION_ID, "Punctuation character", punctuationTextStyle());
		acceptor.acceptDefaultHighlighting(COMMENT_ID, "Comment", commentTextStyle());
		acceptor.acceptDefaultHighlighting(STRING_ID, "String", stringTextStyle());
		acceptor.acceptDefaultHighlighting(NUMBER_ID, "Number", numberTextStyle());
		acceptor.acceptDefaultHighlighting(DEFAULT_ID, "Default", defaultTextStyle());
		acceptor.acceptDefaultHighlighting(INVALID_TOKEN_ID, "Invalid Symbol", errorTextStyle());
	}

	public TextStyle defaultTextStyle() {
		TextStyle textStyle = new TextStyle();
//		textStyle.setBackgroundColor(new RGB(255, 255, 255));
		textStyle.setColor(new RGB(0, 0, 0));
		return textStyle;
	}
〜
}

acceptor.acceptDefaultHighlighting()で、IDに対する名称とスタイルを割り当てている。
ここで割り当てられたスタイルは、Eclipseのpreferences(設定ページ)でユーザーが変更することが出来る。
(ここで付けた名称が設定ページで表示される。設定の保存にはたぶんIDが使われると思う)


字句ハイライトのカスタマイズ

字句ハイライトの仕組みは上記の様になっているので、これらのクラスを自分で作れば独自のハイライト(色付け)を行うことが出来る。

Xtext 2.4.2の場合、ハイライト用の雛形クラスは生成されないので、全て自分で作る必要がある。
これらのクラスはuiプロジェクト内に作る。

uiプロジェクト/src/〜/highlighting/MyLexicalTokenToAttributeIdMapper.java:

package jp.hishidama.xtext.example.ui.highlighting;
import java.util.regex.Pattern;

import org.eclipse.xtext.ui.editor.syntaxcoloring.AbstractAntlrTokenToAttributeIdMapper;
import org.eclipse.xtext.ui.editor.syntaxcoloring.DefaultHighlightingConfiguration;
public class MyLexicalTokenToAttributeIdMapper extends AbstractAntlrTokenToAttributeIdMapper {

	/** 記号類 */
	private static final Pattern PUNCTUATION = Pattern.compile("\\p{Punct}*");
	@Override
	protected String calculateId(String tokenName, int tokenType) {
		if (PUNCTUATION.matcher(tokenName).matches()) {
			return DefaultHighlightingConfiguration.PUNCTUATION_ID;
		}
		if ("'TRUE'".equals(tokenName) || "'FALSE'".equals(tokenName)) {
			return DefaultHighlightingConfiguration.NUMBER_ID;
		}
〜
	}
}

uiプロジェクト/src/〜/highlighting/MyHighlightingConfiguration.java:

package jp.hishidama.xtext.example.ui.highlighting;
import org.eclipse.xtext.ui.editor.syntaxcoloring.DefaultHighlightingConfiguration;
import org.eclipse.xtext.ui.editor.syntaxcoloring.IHighlightingConfigurationAcceptor;
public class MyHighlightingConfiguration extends DefaultHighlightingConfiguration {

	@Override
	public void configure(IHighlightingConfigurationAcceptor acceptor) {
		acceptor.acceptDefaultHighlighting(KEYWORD_ID, "Keyword", keywordTextStyle());
		acceptor.acceptDefaultHighlighting(PUNCTUATION_ID, "Punctuation character", punctuationTextStyle());
		acceptor.acceptDefaultHighlighting(COMMENT_ID, "Comment", commentTextStyle());
		acceptor.acceptDefaultHighlighting(STRING_ID, "String", stringTextStyle());
		acceptor.acceptDefaultHighlighting(NUMBER_ID, "Number", numberTextStyle());
		acceptor.acceptDefaultHighlighting(DEFAULT_ID, "Default", defaultTextStyle());
		acceptor.acceptDefaultHighlighting(INVALID_TOKEN_ID, "Invalid Symbol", errorTextStyle());
	}
}

そして、これらの独自クラスを使うようUiModuleクラスに登録する。
UiModuleクラスは自動生成されているので、必要なメソッドを追加する。

uiプロジェクト/src/〜/ui/MyUiModule.java:

/*
 * generated by Xtext
 */
package jp.hishidama.xtext.example.ui;
import jp.hishidama.xtext.example.ui.highlighting.MyHighlightingConfiguration;
import jp.hishidama.xtext.example.ui.highlighting.MyLexicalTokenToAttributeIdMapper;

import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.xtext.ui.editor.syntaxcoloring.AbstractAntlrTokenToAttributeIdMapper;
import org.eclipse.xtext.ui.editor.syntaxcoloring.IHighlightingConfiguration;
/**
 * Use this class to register components to be used within the IDE.
 */
public class MyUiModule extends jp.hishidama.xtext.example.ui.AbstractMyUiModule {
	public MyUiModule(AbstractUIPlugin plugin) {
		super(plugin);
	}
	public Class<? extends AbstractAntlrTokenToAttributeIdMapper> bindTokenToAttributeIdMapper() {
		return MyLexicalTokenToAttributeIdMapper.class;
	}
	public Class<? extends IHighlightingConfiguration> bindHighlightingConfiguration() {
		return MyHighlightingConfiguration.class;
	}
}

今回追加するメソッドは、メソッド名を「bind」で始め、Classを返すようにする。
こうしておくと、リフレクションによってメソッドを呼び出して初期化して使用してくれるようだ。
(親クラスのAbstractGenericModule#getBindings()の中で それっぽい事をやっている)


構文ハイライト

構文の意味に従ったハイライト(色付け)を行うことが出来る。[2013-08-22]
(セマンティクスは「意味」という訳語みたいだけど、「意味ハイライト」とは言わないような気がする^^;)

構文ハイライトを行う為に、uiプロジェクト内にISemanticHighlightingCalculatorを実装したクラスを用意する。
そして、そのクラスを使用させる為に、UiModuleクラスにメソッドを追加する。

uiプロジェクト/src/〜/highlighting/MyDslSemanticHighlightingCalculator.java:

import org.eclipse.emf.ecore.EObject;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.parser.IParseResult;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.editor.syntaxcoloring.DefaultHighlightingConfiguration;
import org.eclipse.xtext.ui.editor.syntaxcoloring.IHighlightedPositionAcceptor;
import org.eclipse.xtext.ui.editor.syntaxcoloring.ISemanticHighlightingCalculator;
public class MyDslSemanticHighlightingCalculator implements ISemanticHighlightingCalculator {

	//@Override JDK1.5ではインターフェースのメソッドの実装には@Overrideを付けない[2013-08-28]
	public void provideHighlightingFor(XtextResource resource, IHighlightedPositionAcceptor acceptor) {
		→実装例1実装例2実装例3
	}
}

uiプロジェクト/src/〜/ui/MyUiModule.java:

package org.xtext.example.mydsl.ui;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.xtext.ui.editor.syntaxcoloring.ISemanticHighlightingCalculator;
import org.xtext.example.mydsl.ui.highlighting.MyDslSemanticHighlightingCalculator;
public class MyDslUiModule extends org.xtext.example.mydsl.ui.AbstractMyDslUiModule {
	public MyUiModule(AbstractUIPlugin plugin) {
		super(plugin);
	}
	public Class<? extends ISemanticHighlightingCalculator> bindSemanticHighlightingCalculator() {
		return MyDslSemanticHighlightingCalculator.class;
	}
}

字句ハイライトのメソッド追加と同様に、)メソッド名は「bind」で始め(bindの後ろは何でもよい)、Classを返すようにする。


構文ハイライトの例(ノード走査方式)

5分チュートリアルで、name部分にキーワードと同じ色を付けてみる。[2013-08-22]
MyDslSemanticHighlightingCalculatorでノードを走査するよう実装する。

import org.xtext.example.mydsl.myDsl.Greeting;
import org.xtext.example.mydsl.myDsl.MyDslPackage;
	//@Override
	public void provideHighlightingFor(XtextResource resource, IHighlightedPositionAcceptor acceptor) {
		if (resource == null) {
			return;
		}
		IParseResult parseResult = resource.getParseResult();
		if (parseResult == null) {
			return;
		}

		INode root = parseResult.getRootNode();
		for (INode node : root.getAsTreeIterable()) {
			EObject current = node.getSemanticElement();
			if (current instanceof Greeting) {
				List<INode> list = NodeModelUtils.findNodesForFeature(current, MyDslPackage.Literals.GREETING__NAME);
				for (INode n : list) {
					acceptor.addPosition(n.getOffset(), n.getLength(), DefaultHighlightingConfiguration.KEYWORD_ID);
				}
			}
		}
	}

RootNode配下のノードを全て走査する。
node.getSemanticElement()でEObjectを取得し、それがGreeting(というルール定義)であれば
NAME部分にKEYWORD_IDのスタイルを割り当てている。

INodeはトークンの位置(offsetとlength)を保持しているので、それを使う。
割り当てるスタイル(色付けのID)は、字句ハイライトと同じくHighlightingConfigurationに定義されているIDを使用する。


構文ハイライトの例(EcoreUtil方式)

5分チュートリアルで、name部分にキーワードと同じ色を付けてみる。[2013-08-22]
MyDslSemanticHighlightingCalculatorでEcoreUtilを使って構文オブジェクトを走査するよう実装する。

import org.eclipse.emf.ecore.util.EcoreUtil;

import org.xtext.example.mydsl.myDsl.Greeting;
import org.xtext.example.mydsl.myDsl.MyDslPackage;
	//@Override
	public void provideHighlightingFor(XtextResource resource, IHighlightedPositionAcceptor acceptor) {
		if (resource == null) {
			return;
		}

		for (Iterator<EObject> i = EcoreUtil.getAllContents(resource, true); i.hasNext();) {
			EObject current = i.next();
			if (current instanceof Greeting) {
				List<INode> list = NodeModelUtils.findNodesForFeature(current, MyDslPackage.Literals.GREETING__NAME);
				for (INode n : list) {
					acceptor.addPosition(n.getOffset(), n.getLength(), DefaultHighlightingConfiguration.KEYWORD_ID);
				}
			}
		}
	}

EcoreUtilを使って構文オブジェクトを取得している以外は、ノード走査方式と同じ。


構文ハイライトの例(Switch方式)

ハイライトを適用したい構文が増えてくると、上記の方式ではinstanceofのif文が増えてしまう。[2013-08-22]

Xtextが自動生成しているSwitchクラスを使うと構文クラスの判別を行ってくれるので、必要なクラスだけ実装することが出来る。

import org.eclipse.emf.ecore.util.EcoreUtil;

import org.xtext.example.mydsl.myDsl.Greeting;
import org.xtext.example.mydsl.myDsl.MyDslPackage;
import org.xtext.example.mydsl.myDsl.util.MyDslSwitch;
	//@Override
	public void provideHighlightingFor(XtextResource resource, IHighlightedPositionAcceptor acceptor) {
		if (resource == null) {
			return;
		}

		MyDslSemanticSwitch switcher = new MyDslSemanticSwitch(acceptor);
		for (Iterator<EObject> i = EcoreUtil.getAllContents(resource, true); i.hasNext();) {
			EObject current = i.next();
			switcher.doSwitch(current);
		}
	}
	private static class MyDslSemanticSwitch extends MyDslSwitch<Void> {
		private final IHighlightedPositionAcceptor acceptor;

		public MyDslSemanticSwitch(IHighlightedPositionAcceptor acceptor) {
			this.acceptor = acceptor;
		}

		@Override
		public Void caseGreeting(Greeting object) {
			List<INode> list = NodeModelUtils.findNodesForFeature(object, MyDslPackage.Literals.GREETING__NAME);
			for (INode n : list) {
				acceptor.addPosition(n.getOffset(), n.getLength(), DefaultHighlightingConfiguration.KEYWORD_ID);
			}

			return null;
		}
	}

「MyDsl」というDSLの場合、MyDslSwitchというクラスが自動生成されているので、それを継承したクラスを作る。

このクラスには構文ルールに応じたメソッドが用意されているので、必要なものだけオーバーライドする。
例えば「Greeting」ルールの場合、「caseGreeting(Greeting object)」をオーバーライドする。
オーバーライドしたメソッドの中は他の方式と同じ。

参考: stackoverflowのXtext: using the grammar classes in ISemanticHighlightingCalculator


Xtext目次へ戻る / Eclipseへ戻る / 技術メモへ戻る
メールの送信先:ひしだま