VS Code の Elixir Syntax Highlighting を少しカスタマイズ

# VS Code の Elixir Syntax Highlighting を少しカスタマイズ

記事に関連する範囲で自己紹介すると、

* 最近 Elixir の勉強をしている
* ふだん VS Code を使う
* ノートは Markdown で書くことが多い
* [Croma](https://github.com/skirino/croma) という Elixir ライブラリを使う機会がある

という感じです。

この記事の大まかな内容は以下の 2 つです：

* Markdown において Elixir の fenced code block に syntax highlighting したい
* Croma の `defun`, `defunp`, `defunpt` も syntax highlighting したい

VS Code Extension もつくりました。

## Elixir の fenced code block に syntax highlighting

実はこの機能は [vscode-elixir](https://github.com/fr1zle/vscode-elixir) にすでに PR ([#124](https://github.com/fr1zle/vscode-elixir/pull/124)) されていますが、まだ merge されていません。待てないので、以下 2 通りの方法を考えてみます。

* vscode-elixir を fork し、#124 の変更部分を merge し、自分で packaging する。
* vscode-elixir とは別に extension をつくる。

前者のほうが簡単ですが、後述する Croma に関する部分も考慮して extension をつくってしまうことにしました。

### Extension をつくる

公式の [Getting started](https://code.visualstudio.com/api/get-started/your-first-extension) にあるように Yeoman と VS Code Extension Generator を使います。ただし、Getting started では type of extension として `New Extension (TypeScript)` を選択していますが、ここでは `New Language Support` を選択します。質問に答えていくと extension のソースを含む directory が生成されます。

Extension の中身については、上で述べた PR #124 や [vscode-fenced-code-block-grammar-injection-example](https://github.com/mjbvz/vscode-fenced-code-block-grammar-injection-example) がとても参考になります（参考というか、そのまま使えます）。

## Croma の syntax highlighting

ここでは `Croma.Defun` に注目します。このモジュールには型情報を伴う関数を簡潔に定義するためのマクロとして `defun`, `defunp`, `defunpt` があります。やっていることは関数定義なので、`def` や `defp` と同じようにハイライトされて欲しい、というのが私の希望です。

vscode-elixir では、ハイライトのルールは [syntaxes/elixir.json](https://github.com/fr1zle/vscode-elixir/blob/master/syntaxes/elixir.json) に書かれています。関数に関する部分を抜き出すと下記のようになります。

```json
    {
      "comment": "Function with parameters",
      "begin": "\\b(defp?|defmacrop?)\\b\\s*([_$a-z][$\\w]*[!?]?)\\s*\\(",
      "beginCaptures": {
        "1": {
          "name": "keyword.control.elixir"
        },
        "2": {
          "name": "entity.name.function.elixir"
        }
      },
      "end": "\\)",
      "patterns": [
        {
          "include": "$self"
        },
        {
          "include": "#function_parameter"
        }
      ]
    },
    {
      "comment": "Function without parameters",
      "match": "\\b(defp?|defmacrop?)\\b\\s*([_$a-z][$\\w]*[!?]?)",
      "captures": {
        "1": {
          "name": "keyword.control.elixir"
        },
        "2": {
          "name": "entity.name.function.elixir"
        }
      }
    },
```

`(defp?|defmacrop?)` のところを `(defun(p|pt)?)` とすればマッチしそうです。

### Extension に機能を追加する

先に作成した extension に `Elixir-Croma` という言語名で syntax 情報を記述します。元々の `Elixir` との差分は `defun`, `defunp`, `defunpt` だけなので、パターンの大部分は include して使えます。ルールを記述した `elixir-croma.json` は下記のようになります。

```json
{
  "comment": "Syntax Parser for Elixir using Croma.",
  "fileTypes": [
    "ex",
    "exs"
  ],
  "firstLineMatch": "^#!/.*\\belixir",
  "foldingStartMarker": "(after|else|catch|rescue|\\-\\>|\\{|\\[|do)\\s*$",
  "foldingStopMarker": "^\\s*((\\}|\\]|after|else|catch|rescue)\\s*$|end\\b)",
  "name": "Elixir-Croma",
  "patterns": [
    {
      "include": "source.elixir"
    },
    {
      "comment": "Function (Croma) with parameters",
      "begin": "\\b(defun(p|pt)?)\\b\\s*([_$a-z][$\\w]*[!?]?)\\s*\\(",
      "beginCaptures": {
        "1": {
          "name": "keyword.control.elixir"
        },
        "2": {
          "name": "keyword.control.elixir"
        },
        "3": {
          "name": "entity.name.function.elixir"
        }
      },
      "end": "\\)",
      "patterns": [
        {
          "include": "$self"
        },
        {
          "include": "#function_parameter"
        }
      ]
    },
    {
      "comment": "Function (Croma) without parameters",
      "match": "\\b(defun(p|pt)?)\\b\\s*([_$a-z][$\\w]*[!?]?)",
      "captures": {
        "1": {
          "name": "keyword.control.elixir"
        },
        "2": {
          "name": "keyword.control.elixir"
        },
        "3": {
          "name": "entity.name.function.elixir"
        }
      }
    }
  ],
  "scopeName": "source.elixir-croma"
}
```

個人的にポイントだと思っているのは以下の 2 点です：

* `"include": "source.elixir"` で vscode-elixir のパターンを流用している。
* `(defp?|defmacrop?)` を `(defun(p|pt)?)` に変更したため `()` の数が 1 つ増えた。その結果、正規表現における 3 番目のグループが関数名になるため、`"3": {"name": "entity.name.function.elixir"}` とする。

このあたりの詳細は公式の[ここ](https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide)に書いてあります。

## 見た目はこんな感じです

![image.png](https://raw.githubusercontent.com/irisTa56/elixir-tiny-ls/master/image.png)

## おわりに

Language support の VS Code extension は JSON をいじるだけでつくれるのでお手軽です。

* [ElixirTinyLS (VS Code extension)](https://marketplace.visualstudio.com/items?itemName=irisTa56.elixir-tiny-ls)
    * [Repository](https://github.com/irisTa56/elixir-tiny-ls)

## 追記

vscode-icons を使っている場合、ファイルの言語を `Elixir-Croma` に設定したときに Elixir のファイルアイコンが表示されなくなってしまいます。`settings.json` に下記を追加しておく必要があります。このへんの設定を extension settings として簡単に管理できるようにしたいです。。。

```json
    "vsicons.associations.files": [
        {
            "icon": "elixir",
            "languages": [
                {
                    "ids": "elixir-croma",
                    "defaultExtension": "ex"
                },
            ],
            "format": "svg"
        }
    ]
```

## 追々記

この記事では `Elixir-Croma` という新しい言語設定を作成しましたが、言語が変わることによって使えなくなる機能も出てくることが分かりました。結局、既存の Elixir syntax にパターンを追加するほうが良いという結論に至りました。このあたりの詳細は公式の[ここ](https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#injection-grammars)に書いてあります。

自作の VS Code extension もアップデートしてあります。

* [ElixirTinyLS (VS Code extension)](https://marketplace.visualstudio.com/items?itemName=irisTa56.elixir-tiny-ls)
    * [Repository](https://github.com/irisTa56/elixir-tiny-ls)