SublimeLinterを拡張してエラーをチェックする方法


## 1. コンパイルエラー
みなさんはエラーをどのように発見していますか？実行時？テスト？

いろいろあると思いますが、eclipseやXCodeばかり使っているとエラーがリアルタイムに通知されたりするので、ついつい他のエディタにもそのような挙動を期待してしまいます。

やっぱり早い段階でエラー通知してくれたほうが修正はしやすいです。


!["sl01"](http://dl.dropbox.com/u/289852/qiita/04/sl01.jpg)

上の画像はMono Developでエラーを表示しているところです。今回はUnity(C#)のコンパイルをSublime Text2上でリアルタイムで実施する方法を説明します。C#でこの方法を使えばシンタックスのチェックだけでなく、存在しない変数名を使用している場合にエラーを出したり、使用していない変数に対してWarningのメッセージを出したりできます。

他の言語でチェックする仕組みを作る場合も似たようなことをすれば出来るはずです。

## 2. SublimeLinterのインストール
最初に必要なのは[SublimeLinter](https://github.com/SublimeLinter/SublimeLinter)というパッケージです。[Package Control](http://wbond.net/sublime_packages/package_control)を使ってインストールしておきましょう。このパッケージをインストールするだけで、Pythonのエラーはリアルタイムでチェックしてくれます。
このパッケージにはUnityC#の拡張が含まれていないのでこれを実装していきます。

## 3. 拡張の方法
SublimeLinterフォルダのmodulesフォルダにPythonのスクリプトを追加し、その中身を実装していきます。
尚、[SublimeLinterにD言語(他の任意の言語)を追加してハイライト出来るようにする](http://qiita.com/items/c4e29043e808ac3b19e8)という記事がとても参考になりました。

### 3-1. 言語ファイルの新規作成
Packages/SublimeLinter/sublimelinter/modules/に各言語毎の拡張が入っているので、ここにチェックしたい言語のファイルを作成します。objective-j.pyをコピーしてchsarp.pyとして保存します。

※Macの場合、/Users/[ユーザ名]/Library/Application Support/Sublime Text 2/Packages/が対象フォルダです

### 3-2. CONFIG設定
まず、ソースコード中にCONFIGというdictionaryを定義し、そこにチェックの設定を書く必要があります。ここでは"language"をキーとし、使用する言語(UnityC#, Pythonなど)を値とする要素を定義しておきます。ここで定義した言語のときだけ、このファイルが実行されます。

※実行するコマンドを渡して記述するコードを省略することも可能です。詳しい実装はjava.pyを参照してください。

### 3-3. クラスの実装
クラスを作成するときは、BaseLinterを継承してください。このクラスにはError, Warning, Info等のエラーレベル毎の情報を管理するコードが含まれています。

```C#:csharp.cs
# -*- coding: utf-8 -*-
# java.py - sublimelint package for checking java files

import sublime
import subprocess
import os
import os.path
import re

from base_linter import BaseLinter, INPUT_METHOD_FILE

CONFIG = {
    'language': 'UnityC#'
}

class Linter(BaseLinter):
	...
```

### 3-4.メソッドの実装
今回実装するメソッドは以下の2つです。

#### (a) built_in_check(self, view, code, filename):
 * このメソッド内でエラーのチェック処理を実装し、その結果を返します

#### (b) parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages)
 * linesの中にbuild_in_checkで実行したエラーの結果が格納されています
 * 〜UnderLinesおよび〜Messagesのリストには**何も入っていません**
 * linesを1つづつ正規表現で解析しエラーが発生した場合はerrorMessagesにエラー内容を格納します
 * errorUnderlinesには、エラーを表示する対象の[Region](http://www.sublimetext.com/docs/2/api_reference.html#sublime.Region)を格納します
 * Warningの場合はwarning〜、Infoの場合はviolation〜に格納します
 * これら引数で受け取ったものを返す

## 4. built_in_checkの実装
built_in_checkでは対象ファイルのエラーチェック処理を実装します。今回の例ではMono C#のコンパイルコマンドを実行します。

### 4-1. Unity C#のコンパイル方法
Unityをインストールすると、mcsというMono C#のコンパイラがインストールされ、パスが通されているはずなので、これをチェックに使います。
そのままソースコードをコンパイルしてもmainメソッドが無いという理由でコンパイルできないので-target:libraryをオプションを使いましょう。

また、コンパイル対象のソースコードが使用している以下のDLLをインポートする必要があるので、-r:[path],[path]…オプションを使います。

 - UnityEngineのDLL - /Applications/Unity/Unity.app/Contents/Frameworks/Managed/UnityEditor.dll
 - UnityEditorのDLL - /Applications/Unity/Unity.app/Contents/Frameworks/Managed/UnityEngine.dll
 - Unityプロジェクトがコンパイル時に出力するDLL - [プロジェクトパス]/Library/ScriptAssemblies/Assembly-CSharp.dll

最終的に投げられるコードはこんな感じです。

> mcs hoge.cs -target:library -r:/Applications/Unity/Unity.app/Contents/Frameworks/Managed/UnityEngine.dll,/Applications/Unity/Unity.app/Contents/Frameworks/Managed/UnityEditor.dll,[プロジェクトパス]/Library/ScriptAssemblies/Assembly-CSharp.dll


### 4-2. 一時フォルダを作成する
まず、現在編集中のファイルを別のファイルに一時的にファイルとして保存します。以下のコードを記述して一時ファイル用のフォルダを作ります。

```C#:csharp.py
#書いてる途中のファイルを書き出すtempフォルダ
TMPPATH_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '.dtmpfiles'))
if not os.path.exists(TMPPATH_DIR):
    os.mkdir(TMPPATH_DIR)
```

### 4-3. コマンドを実行する
subprocess.Popenを使ってコマンドを実行します。今回作成するのは1ファイルのみのコンパイルの実行とします。built_in_checkの全体の処理は以下のようになります。

```Python:csharp.py
	...
    def built_in_check(self, view, code, filename):
        #パス取得
        work_path = os.path.dirname(filename)
        file_name = os.path.basename(filename)

         #コンパイル場所の設定取得
        settings = view.settings().get('SublimeLinter', {}).get(self.language, {})
        if(settings):
            dwd = settings.get('working_directory', [])
            if(dwd):
                #プロジェクトに設定あったらそれを取得
                work_path = dwd

        #書いてる途中のファイルをtempファイルとしてcsharp.py直下に書き出し
        tempfilePath = os.path.join(TMPPATH_DIR, file_name)

        with open(tempfilePath, 'w') as f:
            f.write(code)

        args = ["mcs", tempfilePath, "-target:library", "-r:/Applications/Unity/Unity.app/Contents/Frameworks/Managed/UnityEngine.dll,/Applications/Unity/Unity.app/Contents/Frameworks/Managed/UnityEditor.dll,[プロジェクトパス]/Library/ScriptAssemblies/Assembly-CSharp.dll"]

        try:
            #コンパイル
            process = subprocess.Popen(args,
                                        cwd=work_path,
                                        stdin=subprocess.PIPE,
                                        stdout=subprocess.PIPE,
                                        stderr=subprocess.STDOUT,
                                        startupinfo=self.get_startupinfo())
            result = process.communicate(None)[0]
        finally:
            if tempfilePath:
                #保存したファイルの削除
                os.remove(tempfilePath)

        return result.strip()
```

## 5. parse_errorsの実装
parse_errorsでは先程のコマンドの実行結果を元に、[Region](http://www.sublimetext.com/docs/2/api_reference.html#sublime.Region)を作成します。

### 5-1. 処理の概要

引数のlinesにはbuilt_in_checkで返されたエラー行が入っています。それをパースして〜Messagesにメッセージを格納し、〜Underlinesに対応するRegionを格納してください。

```
    def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages):
        for line in errors.splitlines():
            print line
            # Template.cs(24,16): error CS0246: The type or namespace name `TemplateFrame' could not be found. Are you missing a using directive or an assembly reference?
            match = re.match(r'^(?P<filename>.+\.cs)\((?P<line>\d+),(?P<col>\d+)\): (?P<level>\w+) (?P<error>.+)', line)

            if match:
                tab_filename = os.path.basename(view.file_name())
                error_filename = os.path.basename(match.group('filename'))
                level = match.group('level')
                error, line = level + ' ' + match.group('error'), match.group('line')
                col = match.group('col')

                messages = None
                underlines = None
                if level == 'warning':
                    messages = warningMessages
                    underlines = warningUnderlines
                else:
                    messages = errorMessages
                    underlines = errorUnderlines

                if(tab_filename == error_filename):
                    self.add_message(int(line), lines, error, messages)
                    self.underline_word(view, int(line), int(col), underlines)
```

###5-2. Sublime Text2のバグ？(補足)
長さが1の[Region](http://www.sublimetext.com/docs/2/api_reference.html#sublime.Region)を作成するとアンダーラインが引かれるのに対し、長さが2以上の[Region](http://www.sublimetext.com/docs/2/api_reference.html#sublime.Region)の場合は背景が塗り潰されます。この原因知ってる方いたら教えてください。.tmThemeを編集しても変化は無く、Pythonの実装でも長さ1の[Region](http://www.sublimetext.com/docs/2/api_reference.html#sublime.Region)を複数配置することでアンダーラインを実現してるみたいです。

## 6. SublimeLinter設定ファイルの編集

次にSublimeLinterの設定を変更します。以下のメニューから設定ファイルを開きます。

>Sublime Text 2->Preferences->Package Settings->SublimeLinter->Settings - Default

開いたら以下の項目を変更してください。

### 6-1. sublimelinter
SublimeLinterを有効にするかどうかを設定できます。**true**に設定します。

### 6-2. sublimelinter_delay
デフォルトの0だとキー入力があった場合に即コマンドが実行され、激重です。これを**5**に設定すると最後のキー入力の5秒後にコマンドを実行します。

### 6-3. sublimelinter_fill_outlines
エラーがある行の周りに線を引きます。今回はエラーがあった箇所だけ強調表示できれば良いので**false**に設定します。

### 6-4. sublimelinter_gutter_marks
行数の表示欄にアイコン(●)を表示します。**true**を設定します。

## 7. Color Schemeの編集

設定ファイルを変更しただけだと、エラー箇所はマークされますがエラーレベル(ErrorとWarningなど)による色付けがされません。Color Schemeの設定を追加してこの問題を解決します。

### 7-1. tmThemeとは

Color Schemeのファイル(.tmTheme)に、エラーレベル毎(Error/Warning/Info)のColor Schemeの設定を追加します。
この.tmThemeファイルは、キーと表示に関する情報を持ちます。

例えば、デフォルトのMonokai.tmThemeには以下のような設定があります。これはscope(キー)がcommentである[Region](http://www.sublimetext.com/docs/2/api_reference.html#sublime.Region)(領域)を描画するときに、settings以下の設定(文字色を#75715Eにする)を使う記述です。

```XML:Monokai.tmTheme
<dict>
	<key>name</key>
	<string>Comment</string>
	<key>scope</key>
	<string>comment</string>
	<key>settings</key>
	<dict>
		<key>foreground</key>
		<string>#75715E</string>
	</dict>
</dict>
```

PythonやRubyなどの言語毎の色付け設定もこのファイルから取得しています。

### 7-2. Color Schemeのファイルを新規作成する
デフォルトのファイルをコピーしてSublimeLinterの設定を追加しましょう。以下のパスに基となるファイルがあるので、

> /Users/[ユーザ名]/Library/Application Support/Sublime Text 2/Packages/Color Scheme - Default/Monokai.tmTheme

Packagesフォルダ下に"Color Scheme - Org"を作ってその配下にコピーしてください。

> /Users/[ユーザ名]/Library/Application Support/Sublime Text 2/Packages/**Color Scheme - Org**/Monokai.tmTheme

### 7-3. .tmThemeファイルにSublimeLinterの設定を追加する
先程コピーした.tmThemeファイルを開き、\<array>〜\</array>の間に以下のコードをコピペしてください。
※[github上の例](https://github.com/SublimeLinter/SublimeLinter)ではWarningの文字色が赤だったので黄色に変更してあります

```XML:.tmThemeに追加
<dict>
    <key>name</key>
    <string>SublimeLinter Annotations</string>
    <key>scope</key>
    <string>sublimelinter.notes</string>
    <key>settings</key>
    <dict>
        <key>background</key>
        <string>#FFFFAA</string>
        <key>foreground</key>
        <string>#FFFFFF</string>
    </dict>
</dict>
<dict>
    <key>name</key>
    <string>SublimeLinter Error Outline</string>
    <key>scope</key>
    <string>sublimelinter.outline.illegal</string>
    <key>settings</key>
    <dict>
        <key>background</key>
        <string>#FF4A52</string>
        <key>foreground</key>
        <string>#FFFFFF</string>
    </dict>
</dict>
<dict>
    <key>name</key>
    <string>SublimeLinter Error Underline</string>
    <key>scope</key>
    <string>sublimelinter.underline.illegal</string>
    <key>settings</key>
    <dict>
        <key>background</key>
        <string>#FF0000</string>
    </dict>
</dict>
<dict>
    <key>name</key>
    <string>SublimeLinter Warning Outline</string>
    <key>scope</key>
    <string>sublimelinter.outline.warning</string>
    <key>settings</key>
    <dict>
        <key>background</key>
        <string>#DF9400</string>
        <key>foreground</key>
        <string>#FFFFFF</string>
    </dict>
</dict>
<dict>
    <key>name</key>
    <string>SublimeLinter Warning Underline</string>
    <key>scope</key>
    <string>sublimelinter.underline.warning</string>
    <key>settings</key>
    <dict>
        <key>background</key>
        <string>#FFFF00</string>
    </dict>
</dict>
<dict>
    <key>name</key>
    <string>SublimeLinter Violation Outline</string>
    <key>scope</key>
    <string>sublimelinter.outline.violation</string>
    <key>settings</key>
    <dict>
        <key>background</key>
        <string>#ffffff33</string>
        <key>foreground</key>
        <string>#FFFFFF</string>
    </dict>
</dict>
<dict>
    <key>name</key>
    <string>SublimeLinter Violation Underline</string>
    <key>scope</key>
    <string>sublimelinter.underline.violation</string>
    <key>settings</key>
    <dict>
        <key>background</key>
        <string>#FF0000</string>
    </dict>
</dict>
```

## 8. 結果
これで以下の画像のように、リアルタイムにエラーチェックを実施できます。SyntaxをCONFIGで設定したものに変更して試してみてください。

行にカーソルを合わせるとステータスバーにそのエラー内容が表示されます。

!["sl02"](http://dl.dropbox.com/u/289852/qiita/04/sl02.png)

 * [Monokai.tmTheme](https://gist.github.com/3699678)
 * [csharp.py](https://gist.github.com/3699696)

## 9. 課題
 * コンパイルのタイミングで打鍵しようとすると少し固まる
 * 本当は別スレッドでコンパイルしてその結果を表示したいが、viewの編集(Regionの追加等)はメインスレッドで実行しないと例外が発生する
 * DLLなどのパスを設定ファイルから読み込む方法(Unityの場合はProjectのルートをST2のプロジェクトに追加して、そこから取得するルールにしたほうがよいかも)
 * ツールチップが出せない(ST2の機能として存在しない)