VSCode用の言語サーバ拡張機能をPythonでつくってみた

# はじめに

本記事は [Visual Studio Code Advent Calendar 2021](https://qiita.com/advent-calendar/2021/vscode) 23日目の記事です。

Visual Studio Code (以下VSCode) 向けにLanguage Server Protocol (以下LSP) を利用した言語サーバ拡張機能を **Pythonで** つくってみました。

**～経緯～**
VSCodeの独自コード補完機能をつくりたい！
Pythonのライブラリをつかいたい！
言語サーバという仕組みを利用しよう！
紹介記事等をググってもJavaScriptで作っている例ばっかり！
しゃーなし、実際にPythonで実装された言語サーバのコード読むか～
あー、そーゆーことね、完全に理解した（わかってない）
後続のために解説記事として残しておこう！

という感じの経緯です。なので言語サーバをJS以外の言語（特にPython）で作ってみたい人の参考になれるように頑張って書いてみます。

あとは、普段コード補完やコードの自動修正機能を使っているけど仕組みは知らなかったな～という人も本記事を読んで理解が深まったらうれしいです。

ちなみにサーバ側をJSで実装している例というのは以下の記事などです。

https://qiita.com/Ikuyadeu/items/98458f9ab760d09660ff

https://qiita.com/vain0x/items/d050fe7c8b342ed2004e

また、参考にさせてもらったPython言語サーバというのはコチラです。
いきなり見てもわかりにくいかもしれないですが、この記事を一通り読んだ後ならだいたい理解できるとおもうので最後に戻ってくるのもアリだと思います。

https://github.com/palantir/python-language-server

# そもそもLSPって？

LSPは先述のとおりLanguage Server Protocolの略で言語サーバを利用するために定義したプロトコル[^1]です。

[^1]: 参考：https://wa3.i-3-i.info/word11.html

じゃあ言語サーバってなに？というと、これはプログラミング言語ごとに固有のコード解析を行う処理系のことです。
たとえばJavaやC#やTypeScriptなんかの型のある言語で、ソースコードを解析して型チェックを行う処理がその一つです。

```typescript:型チェックされる例
const num:integer = "Hello";
// -> 型 'string' を型 'number' に割り当てることはできません。ts(2322)
```

こういった機能を言語サーバとしてエディタから切り離すことで、コード解析機能を複数のエディタで共有できたり、エディタから複数の言語の解析機能を簡単に呼び出せるというメリットがあります。（以下画像参照）

<img width="480" alt="言語サーバの有無による違い" src="https://code.visualstudio.com/assets/api/language-extensions/language-server-extension-guide/lsp-languages-editors.png">
引用元：https://code.visualstudio.com/api/language-extensions/language-server-extension-guide

また、エディタから切り離されているので言語サーバ自体の実装言語はエディタの実装言語に依存しないというのも大きなメリットです。

例えばVSCodeはJS製なのでコード解析等の処理をほかの言語で実装することはできません。
でも言語サーバとしてエディタの外につくることで任意の言語をつかえるので、既存の資産を活用したり、その言語独自の機能を活用することができます。（以下画像参照）
ちなみに、エディタと同じくJSで作ったコード解析機能であれば言語サーバにしなくても拡張機能に組み込めたりします。

<img width="480" alt="言語サーバの位置" src="https://code.visualstudio.com/assets/api/language-extensions/language-server-extension-guide/lsp-illustration.png">
引用元：https://code.visualstudio.com/api/language-extensions/language-server-extension-guide

さて、LSPに話を戻すと、LSPはこの言語サーバとエディタが通信するときのプロトコルで、具体的にはJSON-RPC[^2]を用いたプロトコルとして以下に定義されています。

[^2]: 参考：https://qiita.com/oohira/items/35e6eaaf4b44613ad7d3

https://microsoft.github.io/language-server-protocol/specifications/specification-current/

プロトコルとして「エディタはどのタイミングでどんなリクエストを言語サーバに送るのか」「言語サーバはどんなリクエストに対してどんなレスポンスを返せばいいのか」等が定められているので、この仕様に沿ったエディタや言語サーバを実装すればほかの言語サーバやエディタを利用することができます。

例えばプロトコルの1つに [DidChangeTextDocument Notification](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didChange) という仕様があって、これはドキュメントが変更されたときにエディタから言語サーバに変更されたことと変更内容を通知するという仕様です。なので

- エディタは「ドキュメントを編集したらこのメソッドを言語サーバに送信する」という処理を実装すれば言語サーバのドキュメント変更時の処理を動かせる
- 言語サーバはこのメソッドを受け取ったらドキュメント変更時の処理を実行すればよい

という風にエディタとサーバそれぞれで必要な処理を実装すればドキュメント変更時の機能が利用できます。とってもシンプルですね。

LSPについてさらに詳しく知りたいという方には以下が参考になるかと思います。

https://docs.microsoft.com/ja-jp/visualstudio/extensibility/language-server-protocol

https://qiita.com/atsushieno/items/ce31df9bd88e98eec5c4

# つくったもの

とりあえず、今回つくったコードは以下にまとめています。

https://github.com/www-tacos/lse-in-python

以降では

- VSCode拡張機能のつくりかた

- クライアント側の実装

- サーバ側の実装

- デバッグ/リリースのやりかた

についてそれぞれ解説します。

# VSCode拡張機能のつくりかた

VSCode拡張機能の開発方法は[公式ドキュメント](https://code.visualstudio.com/api/get-started/your-first-extension)に載っているので、これに従えば簡単につくれます。
なのでここでは特に詳しい解説は省略して実行したコマンドや入力内容だけ紹介します。

## 必要なツールのインストール

必要なツールはnpmパッケージで公開されているのでそれぞれグローバルにインストールします。

```txt
> npm install -g yo generator-code vsce
```

- yo : 新規にプロジェクトを作る際のひな型を作ってくれるYeomanというツール →[詳細](https://www.npmjs.com/package/yo)
- generator-code : YeomanのVSCode拡張機能用のテンプレート →[詳細](https://www.npmjs.com/package/generator-code)
- vsce : VSCode拡張機能をパッケージングしたり公開したりするツール →[詳細](https://www.npmjs.com/package/vsce)

ちなみに自分が知らなかっただけかもしれませんが、もしWindowsでnodeのバージョン管理にNodistを使っている場合はnvm等の最新のツールに乗り換えた方がいいらしいです。
Nodistは更新が止まっているらしく、上記コマンドだとgenerator-codeのインストールに失敗してしまいます。

## テンプレートの作成

Yeomanを実行して対話形式で拡張機能の種類や名前を決めていきます。

```txt
> yo code

// 設定した内容
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? LSE in Python
? What's the identifier of your extension? lse-in-python
? What's the description of your extension? A Sample Language Server Extension implemented in Python
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm
```

クライアント側の実装言語はJavaScriptかTypeScriptを選べます。
クライアント側で実装することはほとんどないのでJavaScriptでも問題ないとは思いますが、特に理由がなければTypeScriptの方がいいと思います。

処理が完了すると以下のようなディレクトリが作成されていると思います。

```txt
lse-in-python
├ .vscode
├ node_modules
├ src
│ ├ test
│ └ extension.ts
├ .eslintrc.json
├ .gitignore
├ .vscodeignore
├ CHANGELOG.md
├ package-lock.json
├ package.json
├ README.md
├ tsconfig.json
└ vsc-extension-quickstart.md
```

あとはこの中でクライアント・サーバそれぞれについて実装していくだけです。

# クライアント側の実装

クライアントの本体は`lse-in-python/src/extension.ts`で、ここから言語サーバへの接続を行います。

まずクライアント側で言語サーバを呼ぶためのモジュールをインストールします。

```txt
> npm install vscode-languageclient
```

次に`extension.ts`で必要なモジュールをimportします。

```typescript
import * as path from "path";
//@ts-ignore
import { Executable, LanguageClient, LanguageClientOptions, StreamInfo, ServerOptions } from 'vscode-languageclient';
```

`vscode-languageclient`については自分の環境だとメンバーが見つからないと言われてしまったのでエラー対策で`@ts-ignore`を入れています。

次に言語サーバ拡張機能が起動されたときに実行されるactivate関数にサーバー起動処理を追加します。

```typescript
export function activate(context: vscode.ExtensionContext) {
  // サーバ側の設定を定義する
  const serverPath: string = context.asAbsolutePath(path.join("server", "server.py"));
  const serverOptions: ServerOptions = { command: "python", args: [serverPath] };

  // クライアント側の設定を定義する
  const clientOptions: LanguageClientOptions = {
    documentSelector: [
      { scheme: "file", language: "plaintext" },
      { scheme: "file", language: "python" }
    ]
  };

  // クライアントを作成して起動する
  client = new LanguageClient(
    "lseInPython",
    "Language Server Extension in Python",
    serverOptions,
    clientOptions
  );
  client.start();
}
```

上記の実装では、まずserver/server.pyを言語サーバとして定義してpythonコマンドで実行するように指定しています。なので拡張機能起動時に `python server/server.py` が実行されます。

<details><summary>余談：Pythonコマンドの実行可否について</summary>

---

今回の設定ではコマンドにpythonを指定しているので、PCにPythonがインストールされていて環境変数にパスが通っている必要があります。
もしPythonが未インストールの場合はエラーになってしまうので、事前にコマンドの存在チェック等をして、存在しない場合は右下の通知みたいなやつで「Pythonのインストールが必要です」みたいな表示を出すとユーザーフレンドリーなのかなと思います。

方法としては `which python` とか `where.exe python` が実行できれば確認できると思うので、あとは[こちら](https://stackoverflow.com/questions/43007267/how-to-run-a-system-command-from-vscode-extension)を参考にすればできそうな気がします。

---

</details>

次にクライアント側はプレーンテキストファイルとpythonファイルを言語サーバでの処理対象ドキュメントとして設定しています。なのでそのほかのドキュメント（Java, JS等）を編集しても言語サーバにリクエストは送られないです。

そしてこの設定をつかってLanguageClientオブジェクトを作成し、`start()`メソッドを実行することで言語サーバを起動させてクライアントとの通信を開始します。
詳細はよくわかってないんですが、`start()`でserver.pyを実行するとエディタのプロセスとserver.pyの実行プロセスがパイプで接続されて標準入出力経由でデータを送受信できるっぽいです。ﾊﾟｲﾌﾟ､ﾌﾟﾛｾｽ､ﾜｶﾝﾅｲ...

次に、テンプレートから作成した初期状態だと拡張機能が起動されるタイミングはコマンドパレットから`Hello World`を呼びだした時だけになっているので、エディタを開いたらすぐに起動するように`package.json`を変更しておきます。

```jsonc
//<中略>
  "activationEvents": [
    "onCommand:lse-in-python.helloWorld",
    "*"
  ],
//<中略>
```

これでエディタを開いたら`server/server.py`で実装された言語サーバの起動と接続が行われるようになります。

以上でエディタ側の準備は終了です。エディタ上の画面表示を直接いじる必要がないので楽ですね。

<details><summary>余談：言語サーバとの接続方法について</summary>

---

参考にした言語サーバ拡張機能の[クライアント部分](https://github.com/palantir/python-language-server/blob/develop/vscode-client/src/extension.ts)には言語サーバをIPアドレスで指定して接続する場合のコードもありました。

```typescript
function startLangServerTCP(addr: number, documentSelector: string[]): Disposable {
 const serverOptions: ServerOptions = function() {
  return new Promise((resolve, reject) => {
   var client = new net.Socket();
   client.connect(addr, "127.0.0.1", function() {
    resolve({
     reader: client,
     writer: client
    });
   });
  });
 }

 const clientOptions: LanguageClientOptions = {
  documentSelector: documentSelector,
 }
 return new LanguageClient(`tcp lang server (port ${addr})`, serverOptions, clientOptions).start();
}
```

おそらくローカルホストで言語サーバをたてて接続するというやりかたもあるんだろうなと思います。（というか自分はそっちを想像してました）
標準入出力じゃなくてポート開いてほにゃららみたいな言語サーバを使うときはこっちのやりかたでクライアント側を実装する必要があるんでしょうね。

---

</details>

# サーバ側の実装

サーバ側の本体は`server/server.py`として作成し、この中でソース解析等の処理を行うことにします。

今回のクライアント側の実装だと、サーバ側では標準入出力を使ってJSON-RPCのリクエストとレスポンスを送受信できる必要があります。

いちおう[この記事](https://qiita.com/vain0x/items/d050fe7c8b342ed2004e#%E5%AE%9F%E8%A3%85)のように標準入力を監視すれば自力で実装することもできるようですが、ここは参考にさせてもらった`python-language-server`と同じく`python-jsonrpc-server`を使わせてもらいました。

ということでプロジェクトの中にserverディレクトリをつくって、その中でpipインストールします。

```txt
> mkdir server
> cd server
> touch server.py
> pip install -U python-jsonrpc-server -t ./
```

`server.py`の中ではクライアントから送られてくるリクエストに対してpython-jsonrpc-serverを使って適切なレスポンスを返すという処理を記述していきます。
なのでpython-jsonrpc-serverの仕様についても触れながら説明していきます。

※Python以外の言語で言語サーバを作る場合も、その言語用のJSON-RPCライブラリを使えば考え方自体は同じだと思います。

## python-jsonrpc-serverの利用

server.pyの中でpython-jsonrpc-serverから拝借するモジュールは以下の3つです。

```python
from pyls_jsonrpc.dispatchers import MethodDispatcher
from pyls_jsonrpc.endpoint import Endpoint
from pyls_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter
```

この中でメインになるのが`MethodDispatcher`クラスで、これを継承してリクエスト処理を記述していきます。

MethodDispatcherを標準入出力で使う場合の基本形は以下のような形です。

```python
class SampleLanguageServer(MethodDispatcher):
  def __init__(self):
    self.jsonrpc_stream_reader = JsonRpcStreamReader(sys.stdin.buffer)
    self.jsonrpc_stream_writer = JsonRpcStreamWriter(sys.stdout.buffer)
    self.endpoint = Endpoint(self, self.jsonrpc_stream_writer.write)

  def start(self):
    self.jsonrpc_stream_reader.listen(self.endpoint.consume)

if __name__ == "__main__":
  sls = SampleLanguageServer()
  sls.start()
```

startメソッドでサーバが起動して標準入力でjsonを受け取れるようになります。

このクラスではリクエストのメソッド名に対応したメソッドをオーバーロードすることで処理を追加できます。
たとえば以下のようなJSON-RPCのリクエストを受け取った場合を見てみます。（ちなみにLSP仕様の [DidSaveTextDocument Notification](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didSave) という機能のリクエスト例です。）

```json
{
  "jsonrpc": "2.0",
  "method": "textDocument/didSave",
  "params": {
    "textDocument": {
      "uri": "file:///c%3A/Users/tacos/Desktop/demo.txt"
    },
    "text": "Msg is \"Hello\""
  }
}
```

こんな感じのリクエストが渡ってくるとMethodDispatcherを継承したクラス内の`m_text_document__did_save(self, textDocument=None, **_kwargs)`メソッドが呼ばれます。
メソッド名はリクエストのmethod由来の名前で、引数にはparamsの中身が渡される仕様です。（MethodDispatcher本体に定義あり）
なので、`textDocument/didSave`を含むリクエストに対する処理はm_text_document__did_saveメソッドとしてSampleLanguageServerクラス内に定義すればよいです。

こんな感じでLSPの仕様に対応したメソッドを実装していきます。

## 最初の通信用の処理

エディタと言語サーバが接続されると最初に [Initialize Request](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#initialize) がサーバに飛んできます。
このリクエストに対してサーバは接続成功を知らせることもかねつつ**言語サーバがLSP仕様のどれに対応しているのか**を知らせます。
例えば [DidChangeTextDocument Notification](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didChange) に対応していると知らせることで、エディタ側で変更が発生したタイミングで言語サーバにその内容が送信されるようになります。（知らせないと変更時に何も飛んでこないです。）

今回はサンプルということで

- [DidOpenTextDocument Notification](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didOpen) : ドキュメントを開いたときの機能
- [DidChangeTextDocument Notification](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didChange) : ドキュメントを変更したときの機能
- [DidSaveTextDocument Notification](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didSave) : ドキュメントを保存したときの機能
- [Code Action Request](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_didOpen) : エラーのクイックフィックス機能
- [Completion Request](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_completion) : コードの補完機能
- [Document Formatting Request](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_formatting) : ドキュメントのフォーマット機能

に対応させることにしました。

```python
def m_initialize(self, rootUri=None, **kwargs):
  return {"capabilities": {
    # クイックフィックス機能
    'codeActionProvider': True,
    # コード補完機能
    'completionProvider': {
      'resolveProvider': False,
      'triggerCharacters': ['.', '#']
    },
    # ドキュメントのフォーマット機能
    'documentFormattingProvider': True,
    # 保存や変更時の機能
    'textDocumentSync': {
      'change': 1,  # 変更時のリクエストにファイル内容をすべて含める（2にすると差分のみになる）
      'save': {
        'includeText': True,  # 保存時のリクエストに本文を含める
      },
      'openClose': True,  # 開閉時にイベントを発火させる
    },
    # 多分ワークスペースの切り替わりも監視するみたいな機能？（よくわかってない）
    'workspace': {
      'workspaceFolders': {
        'supported': True,
        'changeNotifications': True
      }
    }
  }}
```

## ドキュメントを開いたときの処理

今回のサンプルでは、ドキュメントを開いたときに「無条件に0行目の0文字目から5行目の5文字目までを警告にする」ことにしました。
以下がその実装です。

```python
def m_text_document__did_open(self, textDocument=None, **_kwargs):
  self.lastDocument = textDocument['text']
  method = "textDocument/publishDiagnostics"
  params = {
    'uri': textDocument['uri'],
    'diagnostics': [{
      'source': 'lse-in-python',
      'range': {
        'start': {'line': 0, 'character': 0},
        'end': {'line': 5, 'character': 5}  # エディタ上では6行目の5文字目までが対象に見える
      },
      'message': '5行目5文字目までのエラー対象',
      'severity': 2,  # 2だと警告、1だとエラー、などなど
      'code': 'didOpen時の処理'
    }]
  }
  self.endpoint.notify(method, params=params)
```

実際の挙動はこんな感じです。

<img width="720" alt="didOpenの挙動" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1435604/38715e5a-ae7c-2081-ddea-567e680ab3cb.gif">

ちなみに、この処理や後述のドキュメント変更時の処理ではエディタ側はドキュメントの状態をサーバに通知するだけなので返り値は不要です。
それで、最後にnotifyメソッドで `textDocument/publishDiagnostics` というmethod名のjsonデータをエディタに飛ばしているのはサーバからエディタへのリクエストで、これでエラーメッセージや警告メッセージをエディタ側に伝えています。
なんでも、didChangeやdidOpenなどのドキュメント更新時によくやる処理には構文解析・型チェックなどの重めの処理が多くて時間がかかりがちなので、リクエスト・レスポンス型ではなくリクエスト・リクエスト型になっているそうです。（何かの記事かドキュメントで知った話なんですけど、ソースが思い出せないです...）

## ドキュメントを変更したときの処理

今回のサンプルでは、ドキュメントを変更したときに「連続する小文字アルファベットをハイライトする」ことにしました。
以下がその実装です。

```python
# 小文字アルファベットキャプチャ用の正規表現
import re
IS_LOWER = re.compile(r'[a-z]+')

def m_text_document__did_change(self, contentChanges=None, textDocument=None, **_kwargs):
  doc = contentChanges[0]['text']
  lines = doc.split("\n")
  diagnostics = []
  for row,line in enumerate(lines):
    matches = IS_LOWER.finditer(line)
    for m in matches:
      diagnostics.append({
        'source': 'lse-in-python',
        'range': {
          'start': {'line': row, 'character': m.start()},
          'end': {'line': row, 'character': m.end()}
        },
        'message': '小文字のアルファベット',
        'severity': 3,  # 3は「情報」、警告の1つ下
        'code': 'didChange時の処理',
        'data': m.group().upper()
      })
  method = "textDocument/publishDiagnostics"
  params = {'uri':textDocument['uri'], 'diagnostics': diagnostics}
  self.endpoint.notify(method, params=params)
```

実際の挙動はこんな感じです。

<img width="720" alt="didChangeの挙動" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1435604/d37830ad-07f0-e28e-16f0-6aa91de2d25c.gif">

小文字アルファベットの連続にマッチする正規表現を定義して使っているので、連続する小文字アルファペット列ごとにハイライトされています。

## ドキュメントを保存したときの処理

今回のサンプルでは、ドキュメントを保存したときに「すべてのハイライトを消す」ことにしました。
以下がその実装です。

```python
def m_text_document__did_save(self, textDocument=None, **_kwargs):
  method = "textDocument/publishDiagnostics"
  params = {'uri':textDocument['uri'], 'diagnostics': []}
  self.endpoint.notify(method, params=params)
```

実際の挙動はこんな感じです。

<img width="720" alt="didSaveの挙動" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1435604/31b80551-f10e-7e1a-1d94-966678ac8f62.gif">

0件の通知データを送れば全てのハイライトを消せます。というよりも送信した通知データが常に全量になるという仕様です。

## エラーのクイックフィックス処理

今回のサンプルでは、「小文字のアルファベットとしてハイライトされている部分を大文字に変換する」ことにしました。
以下がその実装です。

```python
def m_text_document__code_action(self, textDocument=None, range=None, context=None, **_kwargs):
  code_actions = []
  changes = []
  for diag in context['diagnostics']:
    if diag['source'] == 'lse-in-python' and diag['message'] == '小文字のアルファベット':
      changes.append({
        'range': diag['range'],
        'newText': diag['data']
      })
  if len(changes) > 0:
    code_actions.append({
      'title': '小文字を大文字に変換する',
      'kind': 'quickfix',
      'diagnostics': [diag],
      'edit': {
        'changes': {
          textDocument['uri'] : changes
        }
      }
    })
  return code_actions
```

実際の挙動はこんな感じです。

<img width="720" alt="codeActionの挙動" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1435604/fb56d2b9-2478-aa25-b45f-ac81c79973cf.gif">

codeActionはdiagnosticsの1つ1つに対応していて、エラーや警告箇所にマウスオーバーしたり電球マークをクリックしたりすると修正案の候補を出してくれます。
あと今回は試してないのですが、おそらくエラー1つに対して修正案を複数指定することもできると思います。

## コード補完の処理

今回のサンプルでは、「補完候補にaから始まるワードとbから始まるワードの2つを用意する」ことにしました。
以下がその実装です。

```python
def m_text_document__completion(self, context=None, **_kwargs):
  return {
    'isIncomplete': True,
    'items': [{
      'label': "aを入力したときの補完候補",
    },{
      'label': "bを入力したときの補完候補",
      'insertText': "ラベルとは異なる文字列を挿入"
    }]
  }
```

実際の挙動はこんな感じです。

<img width="720" alt="completionの挙動" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1435604/e3006dd1-74c8-54f1-1633-cb9e70f4c245.gif">

補完候補が表示されるタイミングは

- 候補と先頭一致の単語が入力されたとき
- <kbd>Ctrl</kbd>+<kbd>Space</kbd>（macなら<kbd>Cmd</kbd>+<kbd>Space</kbd>）が押されたとき
- initializeの時に `capabilities.completionProvider.triggerCharacters` を指定していたらその文字が入力されたとき

のようにいくつかあります。

また、候補にマッチした後に入力される文字にまったく別の文字を指定することもできるみたいです。例えばJavaで `print` と打ったら `System.out.printf()` が入力される、みたいな補完はこの機能を使ってできそうですね。

## ドキュメントのフォーマットの処理

今回のサンプルでは、フォーマットを選択したときに「ドキュメント中の小文字アルファベットを大文字にする」ことにしました。
以下がその実装です。

```python
def m_text_document__formatting(self, textDocument=None, _options=None, **_kwargs):
  return [{
    'range': {
      'start': {'line': 0, 'character': 0},
      'end': {'line': len(self.lastDocument.split('\n')), 'character': 0}
    },
    'newText': IS_LOWER.sub(lambda m: m.group().upper(), self.lastDocument)
  }]
```

実際の挙動はこんな感じです。

<img width="720" alt="formattingの挙動" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1435604/475d45d6-dca0-9523-c9b9-bdae4553cd87.gif">

ここで初めて見る `self.lastDocument` なるものがありますが、これは最新のドキュメント内容を保持する自作のプロパティで、`m_text_document__did_open` や `m_text_document__did_change` の中で常に更新しています。（今までの例の中では特に関係がなかったので書きませんでした）
実はformattingメソッドのjsonには本文のテキストが入っていないです。なのでサーバ側で最新のドキュメント状態を管理しておく必要があったんですね。

なお参考にしているpython-language-serverさんはWorkspaceクラスをつくってドキュメント群を管理しているようです。
今回は簡単にアクティブドキュメントの最新状態だけを文字列で保持していますが、タブで複数のドキュメントを開くことや複数のワークスペースで同名のファイルを扱うことなんかも考えると、それくらい厳格に管理する必要があるんだと思います。

# デバッグ/リリースのやりかた

ここまで読んでくださった方の中には実際に触っている方もいるかと思うので、最後にデバッグやリリース方法についても紹介したいと思います。

## デバッグ

とりあえず開発中の拡張機能を試したい！という場合は、プロジェクトを開いている状態で<kbd>F5</kbd>キーを押すと開発中の拡張機能が有効になったエディタを開くことができます。（一応確認しておきますがVSCodeで編集している前提です）
それで、例えばextension.ts内で `console.log` を実行すればデバッグコンソールにその内容を表示することができます。

さて、このデバッグモードについては知っている人も多いと思うのですが、そもそも今回の言語サーバ拡張機能はメイン処理がpython側で動いているので残念ながらこの方法があまり使えないです。なので言語サーバ側にロギング処理を入れる必要があります。

というわけで自分はserver.pyの中で以下のようにログを設定してみました。

```python
import logging, os
logging.basicConfig(
  handlers = [
    logging.FileHandler(
      filename = f"{os.path.expanduser('~/Desktop')}/lse-in-python.log",
      encoding='utf-8',
      mode='a+'
    )
  ],
  level = logging.DEBUG,
  format = "%(relativeCreated)08d[ms] - %(name)s - %(levelname)s - %(processName)-10s - %(threadName)s -\n*** %(message)s"
)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
log = logging.getLogger("pyls_jsonrpc")
log.addHandler(console)
```

とにかくわかりやすく、デスクトップにログを生成するようにしました。
最後の方の`log = logging.getLogger("pyls_jsonrpc")`ではpython-jsonrpc-serverの中に仕込まれているログ出力も有効化してログに書き込むように指定しています。
なのでリクエストのjsonだったりレスポンスとして作成したjsonなんかももれなくログに記録されます。
自分の場合、これを見ることでLSPの理解が一気に進んだので、みなさんもぜひ参考にしてみてください。

あと細かいところですが、Windows環境でPythonのloggingを使った場合、ファイル出力時のデフォルトエンコーディングはsjisになるので明示的にUTF-8を指定しておくとよいと思います。

## リリース

VSCodeの拡張機能としてリリースする方法の解説記事は山のようにあるので検索すればすぐに見つかると思います。

ここでは以下の記事を参考にしつつローカルでインストールできるようにvsixファイルにしてみたいと思います。

https://qiita.com/yuu_1st/items/d2d5a18de4859a165260#vsixファイルを作成するための準備

[必要なツールのインストール](#必要なツールのインストール)の章で`vsce`をインストールしているのでコマンドは準備OKです。

拡張機能をテンプレートから作成した場合、パッケージングするために以下の追加作業が必要です。

- README.mdを編集する
- package.jsonにpublisherを追加する

READMEの方はどういう仕組みか知らないですけど最初から入っている文字列が残っていると「ちゃんと書いてください！」みたいなエラーが出るのでちゃんと書きましょう。
publisherの方は自分の名前なんかを入れておきましょう。ちなみに場所はトップレベルならどこでも大丈夫（なはず）です。

あとはパッケージ化するコマンドをプロジェクトのルートディレクトリで実行すればvsixファイルが生成されるので、vscodeで取り込めばインストール完了です。

```txt
> pwd
lse-in-python
> vsce package
```

# さいごに

以上で本記事は終了です。さいごまで読んでいただきありがとうございます。
これほどの文章量の記事は初めて書いたのとアドベントカレンダーの期限がぎりぎりで最後の方は急ぎ足だったので最初と最後で文章の雰囲気や丁寧度が違うかもしれません。その点はご了承くださいm(__)m

今回のサンプルでは触っていないLSPの機能もたくさんありますし、Python実行可否のチェックのような実用化のために必要な処理もまだまだ残っているので、なにか進捗があればまた記事にしたいと思います。