自分はEmacsでScalaを開発している。最近はScala 3がアツいのでEmacsでScala3を書いているのだが、Scala 3固有のシンタックスをうまく認識しないというトラブルが起こり困っている。
具体的には、Scala 3から利用できるOptional Bracesを使っているとき、TABを押下するとインデントが正しく認識されずに、コードが壊れるというものだ。
TL;DR
- 現行の
scala-mode
は正しくOptional Braces Syntaxを処理できない(開発中) - メリットもそんなにないので普通にブレースを書いたほうがいい
- どうしても使いたかったら別のエディタを選ぶしかない
Optional Braces
Optional Bracesとは、Scala 3から導入された新たな構文で、特定の場合には中カッコ(ブレース)を書かなくても良いというものだ:
class Foo: def bar = for x <- 1 to 9 y <- 1 to 9 yield x * y new Foo().bar
これにより、比較的簡潔にクラスやメソッドを書くことができるようになるし、書き味が若干PythonやRubyに近付く(従来通りにブレースを使った記法もできるので、完全に好みで選ぶことができる)。
インデントがうまくいかない
さて、自分はEmacs + lsp-modeを使ってScalaを書いている。EmacsはLSPを経由してMetals(ScalaのLSPサーバ)を呼び出し、これをもとにシンタックスハイライトや補完を行っている。
デフォルトではタブキーを押下したとき、以下のような挙動になる:
- その行のインデントを適切な位置に調整する
- 必要に応じて補完も行う
しかし、このOptional Bracesを使っていると、TABキーを押下したとき、勝手にその行のインデントレベルが0、つまり先頭のスペースが全て削除されてしまい、当然コンパイルも通らなくなる。
これでは困るので、今回はこの問題の原因調査と解決に向けた調査を行うことにした。
TABを押下したときに何が起こるか
ある操作をしているのにおかしなことが発生するときの調査でまず行うのは、describe-key
関数を実行してキーに紐付いている関数を調べることだ。M-x describe-key
してからTAB
を押下する。
すると自分の環境ではcompany-indent-or-complete-common
を呼び出していることがわかった。(Companyとは補完用の候補を表示するためのUIエンジン。) Companyのインデント機能がうまくLSPと連携できずにインデントがおかしくなっているのではないかと思い、Companyのインデントがどのように行なわれているかを調べることにした。
Companyのインデント実装
前述したcompany-indent-or-complete-common
の内部では、インデントのためにindent-for-tab-command
が呼ばれていた。これはTABキーを押下した際に標準的にEmacsが呼び出す標準的な関数だ。
まず、LSPはこのあたりに手を入れてはいなそうなので、既に入っているscala-mode
がobsoleteになっているのではないかと仮説を立て、いったんscala-modeを入れ直すことにした。しかし、現在のバージョンは0.23だったが入れ直しても変化はなかった。つまりscala-modeが仮に原因だったとしてもこれでは直らないことがわかった。また、scala-mode.el
には"A programming mode for the Scala language 2 and 3"
とあるので、Scala 3の文法に対応していないはずはなさそうに思える。
次に、既にコンパイルされているscala-mode
の.elc
ファイルが古いままになっているのではないかと仮説を立てた。これを削除してうまく動作するならこれが原因だったことになる。がしかし.elc
ファイルを削除しても動作は同じだった。
次に、indent-for-tab-command
がscala-mode
をうまく呼び出せていないのではないかと仮説を立てた。scala-mode
が提供するインデント用のコマンドを直接実行して正常にインデントができれば、この仮説が正しいことになるはずだ。scala-mode
が提供しているインデント用関数はscala-indent:indent-line
なので、これをevalして直接実行してみる。しかしこれでも動作は同じだった。
Scala-modeのインデント実装
そうこうしているうちに https://github.com/hvesalai/emacs-scala-mode/issues/160 を発見した。去年あたりからScala 3対応は議論されているものの、 https://github.com/hvesalai/emacs-scala-mode/pull/170/files で今開発が進んでいるという状況だった。
これでは手出しができないので、諦めてオフサイドルールを使わずに従来通りブレースを書くことにした。
オフサイドルールどうなの
という調査結果をScalaを書いている同僚に共有したところ、あまりオフサイドルールに良い印象は持っていないようだった。昔からScalaを書いているユーザで、なおかつOptional Braces Syntaxによるオフサイドルールが大好き、という人はあまりいないのではないか。
オフサイドルールは一見記述が簡潔に見えるというメリットがあるが、どこが開始で終了なのかパースしにくいという問題を抱え込んでしまう。コピペしたときなどに適切なインデント位置を自動的に推論できない(実際、scala-mode
ではそれが難しくてインデントの実装に手間取っている)。YAMLやPythonのコードをペーストしたときにインデント位置が合わずに何度もTABキーを押下した経験は誰しも持ち合わせているはずだ。ちょっと見掛けの複雑さが増えるが、ブレースがある構文のほうがずっと扱いやすいのではないかと思う(なので自分はどちらかといえばYAMLよりJSONが好き)。