最近よく見るライブプレビューアプリをEclipseで作ってみた・基本機能編

#概要

[準備編](https://qiita.com/HiroInTFLab/items/e40bef536e8f15211615)に続いて、ライブプレビューの基本的な動作までを書きます。
試行錯誤と断片的な情報をつなぎ合わせて構築しました。
誰かの役に立てば幸いです。

#画面イメージ

基本機能編を最後までこなしたイメージは以下の感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/a66ad946-cf30-e524-1273-e34c28c3efd1.png)
基本的な構成は準備編で得られたものをベースとして、追加するところは、

* ウィンドウ右下にプレビューを表示する目的の、PreviewウィンドウをPropertiesより左に追加
* Previewウィンドウには、アクティブエディタの内容に応じた変換結果をリアルタイムに表示する動作を追加

になる。

#開発手順

## 確認用のxtextプロジェクトを作る

ライブプレビュー確認目的で用意するため、名称は凝ったものである必要はなく、
Project nameは```org.xtext.example.md2```
Language Nameは```org.xtext.example.Md2```
Language Extensionは```md2```
とマークダウン風にした。
Finishボタンをクリック後、エディタが起動しMd2.xtextファイルが開くことを確認する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/46370f13-291f-2749-383a-9cc0e8bd18ec.png)

あくまでサンプルなので、文法定義は初期のまま、
<details><summary>初期文法定義</summary><div>

```java
grammar org.xtext.example.mydsl.Md2 with org.eclipse.xtext.common.Terminals

generate md2 "http://www.xtext.org/example/mydsl/Md2"

Model:
	greetings+=Greeting*;
	
Greeting:
	'Hello' name=ID '!';
```
</div></details>
でよい。

## Md2成果物を生成する

エディタ画面上でコンテキストメニュー -> Run -> ```Generate Xtext Artifact```をクリック。
途中、antlrのダウンロードをする旨のメッセージがconsoleウィンドウに出たら、y+Enterを入力（するとダウンロードが始まる）。
ダウンロードできない場合は、URLが表示されるので、メインメニュー -> Help -> Install new softwareから、表示されたURLを指定する方法もある。

## コンテンツ生成クラスを用意する

Previewウィンドウで表示する内容を生成する、コンテンツ生成を担うクラスを実装する。
srcフォルダ直下にある、既存のパッケージ```org.xtext.example.mydsl.generator```に```Md2ContentesGenerator.java```を追加する。
さて、初期文法定義では、
```Hello スペース 英数字の文字列 スペース !```
となっている。
上記の文法定義に従って入力されたソースを、ソースとは異なる文字列に変換する実装を次に行う。といっても、サンプルであるため、末尾に!をひとつ追加して!!となるよう変換するだけに留める。そのソースは以下のようになる。

<details><summary>ソース</summary><div>

```java
public class Md2ContentsGenerator {

	@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();
	}

}
```
</div></details>


## エラーなきことを確認する

Previewではファイル生成は必要ない想定であるものの、xtextプロジェクト生成時にデフォルトで生成されるGeneratorに追記して、xtendファイルからjavaファイルが生成される（エラーだとjavaファイルは生成されない）、程度の確認を行う。
srcフォルダ直下にある、既存のパッケージ```org.xtext.example.mydsl.generator```にある```Md2Generator.xtend```に、不要部分のコメントアウトおよびMd2ContentsGeneratorクラスのインスタンス生成および呼び出しを追加しておく。

<details><summary>Md2Generator.xtend</summary><div>

```java
class Md2Generator extends AbstractGenerator {

	override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) {
//		fsa.generateFile('greetings.txt', 'People to greet: ' + 
//			resource.allContents
//				.filter(Greeting)
//				.map[name]
//				.join(', '))
		if (!resource.contents.empty) {
			var gen = new Md2ContentsGenerator
			var contents = gen.doGenerate(resource)
		}
	}
	
}
```
</div></details>

## Preview用にパッケージとクラスを作成

新たに```test.rcp.part```パッケージを作成し、さらに、```Preview```クラスを作る

## Preview用のPartDescriptiorsを作成する

Application.e4xmiに、Preview用のPartDescriptiorsを作成する
Application.e4xmiを開き、PartDescriptiorsを選択し、+マークのAddボタンをクリック
PartDescriptionがひとつ足されることを確認
LabelにPreviewを入力
ClassURIの右にあるFindボタンをクリックし、先程追加したPreviewクラスを指定する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/571d0154-e3f7-6df5-9673-237d508ce546.png)
追加後はこんな感じになる。

## Preview用のPartを作成する

Application.e4xmiに、Preview用のPartを作成する
Windows -> TrimmedWindow -> Shered Elementsを選択し、Partのまま+ボタンをクリック
先程と同様に、LabelとClassURIを指定する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/746e2c2f-6791-1091-fd32-1b80af30f5e5.png)
追加後はこんな感じになる。

## Preview用のPlaceholderを追加する

Application.e4xmiに、Preview用のPlaceholderを追加する
Windows -> TrimmedWindow -> Controls -> Perspective Stack -> Perspective -> Part sash container -> Part sash container -> Part Stack
で、PartsはPlaceHolderを選択し、+ボタンをクリック
Referenceの右にあるFindボタンをクリックし、Previewを選ぶ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/8bcc7a4c-680a-06c6-42b3-97d6693b5f94.png)
追加後はこんな感じになる。

## Previewウィンドウを実装する

検討した内容や実装した機能を全てを書くと長くなるため、エディタでの編集結果をリアルタイムに変換してPreviewウィンドウに反映する機能の概要だけに割愛すると…

* Previewウィンドウにはテキストボックスを設ける
* テキストボックスは、複数行指定、読み取り専用、水平垂直スクロールバーは有効、とする
* テキストボックスの背景色は、白とする（エディタ画面の背景色と合わせる）
* Previewウィンドウへの反映のイベントは、エディタの切り替わり時、入力内容変化時とする
* エディタの切り替わり時は、アクティブ時、ロード時、アンロード時とする
* 入力内容変化時は、モデル変化時で、かつ、入力内容にエラーがない場合とする

<details><summary>Previewウィンドウソース</summary><div>

```java
public class Preview {
	@Inject
	private EObjectAtOffsetHelper helper = new EObjectAtOffsetHelper();
	private Text text = null;
	private ArrayList<IEditorPart> editors = new ArrayList<IEditorPart>();
	public Preview() {
		IWorkbenchWindow workbenchWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
        workbenchWindow.getPartService().addPartListener(new IPartListener() {
			@Override
			public void partOpened(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);
							}
						});
						ITextSelection selection = (ITextSelection) x.getSelectionProvider().getSelection();
						final int offset = 0;
						EObject object = d.readOnly(new IUnitOfWork<EObject, XtextResource>() {
							// @Override
							public EObject exec(XtextResource state) throws Exception {
								return helper.resolveContainedElementAt(state, offset);
							}
						});
						Resource r = object.eResource();
						if (r instanceof XtextResource) {
							setText((XtextResource) r);
						}
					}
				}
			}
			@Override
			public void partDeactivated(IWorkbenchPart arg0) {
			}
			@Override
			public void partClosed(IWorkbenchPart arg0) {
		    	if (arg0 instanceof XtextEditor) {
		    		if (editors.contains(arg0)) {
		    			XtextEditor x = (XtextEditor)arg0;
		    			editors.remove(x);
		    			if (editors.isEmpty()) {
		    				setText("");
		    			}
		    		}
		    	}
			}
			@Override
			public void partBroughtToTop(IWorkbenchPart arg0) {
			}
			@Override
			public void partActivated(IWorkbenchPart arg0) {
				if (arg0 instanceof XtextEditor) {
					XtextEditor x = (XtextEditor) arg0;
					ITextSelection selection = (ITextSelection) x.getSelectionProvider().getSelection();
					final int offset = selection.getOffset();
					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);
						}
					});
					Resource r = object.eResource();
					if (r instanceof XtextResource) {
						setText((XtextResource) r);
					}
				}
			}
		});
	}
	@PostConstruct
    public void createControls(Composite parent) {
		text = new Text(parent, SWT.BORDER | SWT.MULTI | SWT.READ_ONLY | SWT.H_SCROLL | SWT.V_SCROLL);
	}
    private void setText(XtextResource resource) {
    	if (text != null && !text.isDisposed()) {
        	if (!resource.getParseResult().hasSyntaxErrors()) {
        		StringBuffer sb = new StringBuffer(10);
        		Md2ContentesGenerator　c = new Md2ContentesGenerator();
        		sb.append(resource + "\n");
        		sb.append(c.doGenerate(resource));
            	text.setText(sb.toString());
        	}
    	}
    }
    
    private void setText(String str) {
    	if (text != null && !text.isDisposed()) {
    		text.setText(str);
    	}
    }
}
```
</div></details>

## 確認する

Run -> Debug ...で起動
File -> Newでtestフォルダおよびtest.md2ファイルを生成する。
Xtext natureに関しての質問がダイアログボックスで出た場合は、Yesをクリック。
Ctrl + SpaceでHelloが入力補完されることを確認し、エラー指摘が消えるよう、続けて以下のような入力を行う

```Hello Preview !```

入力後、Previewウィンドウに
```Hello Preview !!```
が表示されることを確認できれば完了。

以上、簡易的ではあるが、ライブプレビューアプリケーションを作ってみた。

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

# 振り返り
## エディタが開いたことをキャッチするには
エディタはEclipseが管理するものであろう、また、Eclipseにはエディタが開いた/閉じたなどへリスナが登録可能ではないか、と勝手に予測し検索。結局、エディタが開いたことのリスナおよびリスナ登録は、[これ](https://stackoverflow.com/questions/542338/how-do-i-get-notified-whenever-a-new-editor-is-opened-in-eclipse)にあるコメントのとおり、
```page.addPartListener(new IPartListener()...```
で可能とわかった。
有益な情報にたどり着くのに時間がかかったので、もう少し検索能力を上げたい。

## xtextエディタの更新をキャッチするには
当初、xtextエディタの継承元になっているエディタクラスに入力のリスナが登録できるものと考えていたが、どうやらそれは悪手らしく、使えそうなのは[これ](https://www.eclipse.org/forums/index.php/t/1080063/)だけだった。
とはいっても、IXtextModelListenerが所望のリスナであることはわかったが、ixtextdocumentとの関係は？また、エディタクラスからIXtextDocumentを取得するには？
などが、これだけ読んでもなんともわからない。
デバッグでメソッド一覧などを出しながら試行錯誤したところ、XtextEditorクラスにIXtextDocumentを取得するgetDocumentメソッドがあった。また、IPartListenerのインスタンスに引数で渡されてくるのが、XtextEditorクラスのインスタンスであることがわかり、IXtextModelListenerのインスタンスを経由して、エディタ取得からエディタへのリスナ登録（つまり、エディタの更新のキャッチ）につながった。

## Partの初期化タイミング
勝手なイメージで、UIに関わるクラスのインスタンスはプラグインのロード時などに生成されるもの、と思っていたが、違った。少なくともe4のPartのインスタンスは**ウィンドウが初めてアクティブになる直前**ということが検討の結果、分かった。
ライブプレビューの実現で困るのは、エディタが開いたあとにPreviewがアクティブになるケース。このケースだと、エディタへのリスナ追加ができない、すなわち、Previewウィンドウが更新されない、点である。
回避方法として、Application.e4xmiで表示順を変えることでアプリ起動直後にPreviewがアクティブになるようにした。

## エディタと関係するResourceの取得方法
[これ](http://www.ne.jp/asahi/hishidama/home/tech/eclipse/xtext/eobject.html#h_EObjectAtOffsetHelper)と[これ](http://www.ne.jp/asahi/hishidama/home/tech/eclipse/xtext/resource.html#h_EObject)を参考にした。
なお、上記リンクで書かれているコードそのままだと、編集途中（ファイル保存していない状態）のエディタへ切り替えを行うと、```document.readOnly```メソッドの戻り値がnullになることがわかった。
デバッグで検討したところ、```selection.getOffset();```の戻り値は、どうやらエディタ内のカーソル位置、のようである。```document.readOnly```メソッドは、対応ファイル内のoffset位置に対応するオブジェクトがない場合に、戻り値としてnullを返すものと思われる。対応ファイル内にないものを指定できないとすると、先頭または**ファイルの末尾**が考えられる。末尾を取る方法がすぐに思いつかなかったため、先頭ということでoffsetを0にしてみたところ、とりあえずnullを回避することはできた。が、他にいい方法がないだろうか…。

## 言語依存の除去は見送り
md2言語のコンバータを直接呼ぶコードのため、Previewの汎用性がほぼゼロである。
よって、xtextでただ言語定義を足すだけ、という状況にはなっていない。
次のステップで対処したい。