denols/typescript-lsのLSPスイッチングは意外と気を遣うよという話
Language Server使ってますか
Language Server便利ですよね。
Vim界隈が端緒となって生まれたLSPですが、今やVisual Studio Codeが標準でサポートしていることもあり、
エンジニアを支える重要な技術です。
denols v.s. typescript-ls
ところで、Language Serverと呼ばれるところからして、サーバーは各言語に対して提供されています。
しかし、言語が同じだからといって、その解釈(意味)が単一であるとは限りません。
最近特に大きな分流が起きているのがおそらくDenoです。
denolsとtypescript-lsはともにTypeScriptを対象にしているため、ごく一般的な識別方法としての「拡張子」が役に立ちません。
どちらもfoo.tsのように、同じ拡張子を持っています。
それゆえに「このファイルがどちらのLanguage Serverで処理されるべきか」の判定には一工夫が必要となっています。
見分け方
悲しい話ですが、結論からいうと確実・決定的な見分け方は存在しません。
しかし、多くのDeno Projectには同階層ないし親階層のディレクトリに次のようなファイルの存在する可能性が高いです。
- deno.json
- deno.jsonc
- deps.ts
また、typescript-lsを利用する場合には以下の様なファイル(ディレクトリ)の存在する可能性が高いでしょう。
- package.json
- tsconfig.json
- node_modules/
ここでさらに一工夫
さて、これは私の意思決定ミスに起因するのですが、とあるプロジェクトでは次のような構成になっています。
- 
{project-root}/- node_modules/
- package.json
- hoge.ts
- fuga.ts
- 
{subproject}/- deno.json
- foo.ts
- bar.ts
 
 
        R    E    P    L    A    Y
お わ か り い た だ け た だ ろ う か
さらに、最近のDenoは node_modules ディレクトリを作ってしまうということもあり、この2つはシンプルに解決しようとすると事故の元なのです。
このような不埒なプロジェクトをいなすためには、次のような慎重さが求められます。
- 現在の階層に
- denols系のファイルが存在する→denols
- typescript-ls系のファイルが存在する→typescript-ls
- それ以外の場合→2へ
 
- 1つ上の階層へ遡る
- 1から繰り返す
実際には、LSPのrootdirを指定する際などには、例えばdenolsの設定では
- denols関係のファイルが見つかるまで現在のファイルパスから遡る
- typescript-ls関係のファイルを見つけた段階で走査打ち切り
みたいな挙動になるでしょう。
宣伝
これを簡便化するためのライブラリclimbdir.nvimを作ってあります。
Neovimユーザーで困ってる人は使ってみてください。利用しているのが私だけ(かつ本件の解決のためにしか使ってない)ので作りは荒いですが…
こんな感じで利用できます。
    root_dir = function(path)
      local marker = require("climbdir.marker")
      -- package.json, node_moduelsを見つけた階層を取得する
      return require("climbdir").climb(path, marker.one_of(marker.has_readable_file("package.json"), marker.has_directory("node_modules")), {
        -- ただし、deno.jsonを見つけたらその階層で探索を打ち切る
        halt = marker.one_of(marker.has_readable_file("deno.json"), marker.has_readable_file("deno.jsonc"))
      })
    end,
気が向いたら使ってみてください。

Discussion