最近よく見るライブプレビューアプリをEclipseで作ってみた・複数言語編

#概要

複数言語編では、[基本機能編](https://qiita.com/HiroInTFLab/items/f6dc7f094d1c49c8c158)の振り返りであげた言語依存部分の解消することで、複数言語への対応の手順を明確にすることを中心に、Previewウィンドウの表示位置に柔軟な機能実現、を追加で実現してみました。
誰かの役に立てば幸いです。

#画面イメージ

複数言語編を最後までこなしたイメージは以下の感じ。
基本機能編で作った確認用の言語のライブプレビュー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/2243811a-5921-5439-bb30-ba311e65ead9.png)
2つ目の言語でのライブプレビューは、!マークが末尾に4個表示されるようにした
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/1141a5f9-ab58-711c-6193-cec359fe4145.png)

#開発手順

基本機能編をベースとして、ExtensionPointの定義により言語依存の解消を行った。概要は、

* 外部からコンテンス生成クラスを定義する、ExtensionPointの定義
* ExtensionPointの定義に基づいて、確認用の言語を更新
* Previewウィンドウは、ExtensionPointの定義のインスタンスによるコンテンツ生成とプレビュー内容更新

になる。

## Extension Pointの定義
### Plug-inProjectを作る
名称は```test.extension```とした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/08bb078c-a3a6-725d-9e12-fefab74dc131.png)
～UIのチェックボックスはチェック、
Rich Client ApplicationはNoを選択、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/ecbbb5be-99b0-437b-83b3-da8c6e887c33.png)
Creating a plug-in using one of the templatesのチェックボックスをチェック、
Hello World Commandを選んでFinishボタンをクリック。
以上の選択とすることで、余計な記述やファイルがあるものの、plugin.xmlなど必要なファイルがデフォルトで生成されるのはありがたい。

### 不要なファイルを削除
srcフォルダに有るパッケージはファイルごと削除
iconsフォルダを削除
plugin.xmlを開き、plugin.xmlタブをクリックし、pluginタグの中身をすべて削除
これで、素のプラグインプロジェクトとなった

### schemaによるExtension Pointを定義

plugin.xmlを開き、Extension Pointsタブをクリック
Addボタンをクリックし、IDはtest、Nameはtest、と入力し、Finishボタンをクリック
現れたschemaの編集画面で、Definitionタブをクリック
New Elementボタンをクリック、NameはcontentGeneratorとした
contentGeneratorをアクティブにして、New Attributeボタンをクリック、Nameはlanguageとした
New Attributeボタンをクリック、Nameはclass、Typeはjava、ImplementsはIContentsGeneratorを入力
Implements文字をクリックし、Packageにtest.extensionを入力し、Finishボタンをクリック

### メソッド追加
IContentsGeneratorschena.javaを開き、以下のように実装する。

```java
public interface IContentsGenerator {
	public String doGenerate(Resource resource);
}
```

## Extension Point使用に更新
確認用に作成したorg.xtext.example.md2言語を、作成したExtension Pointを使用した実装に更新する

### Extension定義を追加
plugin.xmlを開き、plugin.xmlタブをクリックし、以下の記述を追加する。

```java
 <extension
       point="test.extension.test">
    <contentGenerator
          class="org.xtext.example.mydsl.generator.Md2ContentsGenerator"
          language="org.xtext.example.mydsl.Md2">
    </contentGenerator>
 </extension>
```

### ContentsGeneratorを更新
org.xtext.example.mydsl.generatorパッケージの、Md2ContentsGeneratorを以下のようにする。

```java
public class Md2ContentsGenerator implements IContentsGenerator {

	@Override
	public String doGenerate(Resource resource) {
		StringBuilder sb = new StringBuilder(10);
		EObject obj = resource.getContents().get(0);
		if (obj instanceof Model) {
			Model model = (Model)obj;
			for(Greeting g :model.getGreetings()) {
				String hello = "Hello " + g.getName() + "!!\n";
				sb.append(hello);
			}
		}
		return sb.toString();
	}

}
```

## Previewを更新
言語依存の解消のため、Eclipseが起動時に読み込んだプラグインの設定から、言語名が一致したプラグインのインスタンスを呼び出すようにする。
そのコードは以下のようになる。

```java
			IConfigurationElement[] config = Platform.getExtensionRegistry()
					.getConfigurationElementsFor(GENERATOR_EXTENSION_ID);
			try {
				for (IConfigurationElement e : config) {
			final String s = e.getAttribute(GENERATOR_EXTENSION_LANGUAGE);
					if (s.contentEquals(name)) {
						Object o = e.createExecutableExtension(GENERATOR_EXTENSION_CLASS);
						if (o instanceof IContentsGenerator) {
							generatorCache.put(name, o);
							break;
						}
					}
				}
			} catch (CoreException ex) {
				System.out.println(ex.getMessage());
			}
```


他に、
* インスタンスの生成タイミングによるライブプレビューできない現象の解決
* Abstractクラスとその利用によるコードの見通しの良化
* 各箇所にキャッシュを設けて、読み込み回数の最適化
など、細かい修正も施した。

<details><summary>Preview.java全体</summary><div>

```java
public class Preview {

	private Text text = null;
	private ArrayList<IEditorPart> editors = new ArrayList<IEditorPart>();
	private static final String GENERATOR_EXTENSION_ID = "test.extension.test";
	private static final String GENERATOR_EXTENSION_LANGUAGE = "language";
	private static final String GENERATOR_EXTENSION_CLASS = "class";
	private Map<String, Object> generatorCache = new HashMap<String, Object>();
	private Map<Object, String> contentCache = new HashMap<Object, String>();

	public Preview() {
		IWorkbenchWindow workbenchWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
		workbenchWindow.getPartService().addPartListener(new AbstractPartListener() {

			@Override
			public void partOpened(IWorkbenchPart arg0) {
				entryEditor(arg0);
			}

			@Override
			public void partClosed(IWorkbenchPart arg0) {
				eraseEditor(arg0);
			}

			@Override
			public void partActivated(IWorkbenchPart arg0) {
				replaceEditor(arg0);
			}
		});
	}

	@PostConstruct
	public void createControls(Composite parent) {
		text = new Text(parent, SWT.BORDER | SWT.MULTI | SWT.READ_ONLY | SWT.H_SCROLL | SWT.V_SCROLL);
		text.setBackground(new Color(parent.getDisplay(), 255, 255, 255));
	}

	private void entryEditor(IWorkbenchPart arg0) {
		if (arg0 instanceof XtextEditor) {
			if (!editors.contains(arg0)) {
				XtextEditor x = (XtextEditor) arg0;
				editors.add(x);
				IXtextDocument d = x.getDocument();
				d.addModelListener(new IXtextModelListener() {
					@Override
					public void modelChanged(XtextResource resource) {
						setText(resource);
					}
				});
				refreshText(getResource(x));
			}
		}
	}

	@Focus
	public void entryActiveEditor() {
		IEditorPart e = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor();
		entryEditor(e);
	}

	private void refreshText(Resource resource) {
		if (resource == null) {
			setText("");
		} else if (resource instanceof XtextResource) {
			setText((XtextResource) resource);
		}
	}

	private Resource getResource(XtextEditor x) {
		EObjectAtOffsetHelper helper = new EObjectAtOffsetHelper();
		int offset = 0;
		IXtextDocument d = x.getDocument();
		EObject object = d.readOnly(new IUnitOfWork<EObject, XtextResource>() {
			@Override
			public EObject exec(XtextResource state) throws Exception {
				return helper.resolveContainedElementAt(state, offset);
			}
		});
		if (object != null) {
			return object.eResource();
		}
		return null;
	}

	private void eraseEditor(IWorkbenchPart arg0) {
		if (arg0 instanceof XtextEditor) {
			if (editors.contains(arg0)) {
				XtextEditor x = (XtextEditor) arg0;
				editors.remove(x);
				if (contentCache.containsKey(getResource(x))) {
					contentCache.remove(getResource(x));
				}
				if (editors.isEmpty()) {
					refreshText(null);
					contentCache.clear();
				}
			}
		}
	}

	private void replaceEditor(IWorkbenchPart arg0) {
		if (arg0 instanceof XtextEditor) {
			XtextEditor x = (XtextEditor) arg0;
			refreshText(getResource(x));
		}
	}

	private void setText(XtextResource resource) {
		if (text != null && !text.isDisposed()) {
			if (!resource.getParseResult().hasSyntaxErrors()) {
				contentCache.put(resource, getContent(resource));
			}
			String content = contentCache.get(resource);
			if (content == null) {
				content = "";
			}
			text.setText(content);
		}
	}

	private String getContent(XtextResource resource) {
		String name = resource.getLanguageName();
		String content = "";
		if (!generatorCache.containsKey(name)) {
			IConfigurationElement[] config = Platform.getExtensionRegistry()
					.getConfigurationElementsFor(GENERATOR_EXTENSION_ID);
			try {
				for (IConfigurationElement e : config) {
			final String s = e.getAttribute(GENERATOR_EXTENSION_LANGUAGE);
					if (s.contentEquals(name)) {
						Object o = e.createExecutableExtension(GENERATOR_EXTENSION_CLASS);
						if (o instanceof IContentsGenerator) {
							generatorCache.put(name, o);
							break;
						}
					}
				}
			} catch (CoreException ex) {
				System.out.println(ex.getMessage());
			}
		}
		if (generatorCache.containsKey(name)) {
			Object o = generatorCache.get(name);
			content = ((IContentsGenerator) o).doGenerate(resource);
		}
		return content;
	}

	private void setText(String str) {
		if (text != null && !text.isDisposed()) {
			text.setText(str);
		}
	}
```

</div></details>

## 2つ目の確認用言語を追加

確認用の言語定義をもう1つ追加しておく。
言語の拡張子はMd3とした。
Md2から変えた点は、ContentGeneratorが生成する文字列で、末尾に付く！が2個から4個に、だけとした。

```java
public class Md3ContentsGenerator implements IContentsGenerator {

	@Override
	public String doGenerate(Resource resource) {
		StringBuilder sb = new StringBuilder(10);
		EObject obj = resource.getContents().get(0);
		if (obj instanceof Model) {
			Model model = (Model)obj;
			for(Greeting g :model.getGreetings()) {
				String hello = "Hello " + g.getName() + "!!!!\n";
				sb.append(hello);
			}
		}
		return sb.toString();
	}

}
```

## 確認する

Run -> Debug Configurations ...でダイアログを出し、Pluginタブにて、
test.extensionプラグインと、Md3関連のプラグインのチェックボックスにチェックを入れて、Debugボタンをクリック。

起動後、File -> Newでtestフォルダ内におよび拡張子md3のファイルを生成し、意図通りにPreviewウィンドウに反映されることを確認する。
以上、簡易的ではあるが、複数言語に対応可能なライブプレビューアプリケーションを作ってみた。

# 成果物
以上の作業で生成されたものを[GitHub](https://github.com/tfLabs/live3.git)に登録した。

# 振り返り
## なぜExtension Point？
最上の選択肢は、xtextが持っている機能をPreviewウィンドウから利用して解決すること、と考えていたが、調査・検討の結果、Generatorのみを呼び出す方法がなさそうなことが分かった（コマンドラインからファイル生成を伴うGenerator呼び出しがあるが、ライブプレビューには不向き…）。
他にいい選択肢が思いつかなかったため、Extension Pointを定義・利用する方式を採用することとした。
Extension Point定義で参考にしたのは[これ](https://www.vogella.com/tutorials/EclipseExtensionPoint/article.html)

## 言語定義に工夫の余地あり
Extension Point定義とインターフェイスだけのスカスカなプラグインを作ってしまったが、Unicode定義などを組み込んだ言語（基礎言語と称する）をxtextで定義し、Preview表示に対応したい言語は基礎言語をMixInする運用とすれば、プラグイン定義にちゃんと意味を持たせられそうだ。