VSCodeでLSP(Extensionの実装方法を調べる編)

[LSP]VSCodeでLSP(クライアント側のサンプルを読む編)で LSPのクライアント側の実装を見てみたものの、
Extension の実装も見ておかないと理解できなさそうだったのでこちらも見てみる。
参照したのは以下。
https://code.visualstudio.com/docs/extensions/example-hello-world

下準備

Node.js のインストール。
そのあと npm で yo と generator-code をインストールする。

npm install -g yo generator-code

Extension のテンプレートの生成

テンプレート生成したいフォルダに移動して以下を実行

yo code

最初にテンプレートの種類を聞かれるので「New Extension(TypeScript)」を選択。
後はExtension名とかいろいろ。
ちなみに、Nodist を使って Node.js をインストールしてた時は、
「Trying to copy from a source that does not exist:…」
というエラーが出た。
Nodist をやめて普通に Node.js をインストールして使うと正常に実行できた。

実行してみる

VSCode を立ち上げて生成したフォルダを開き、F5 を実行すると、
拡張機能開発ホストが立ち上がる(VSCode の別インスタンス)。
その状態で Ctrl + Shift + P で "Hello World" と打つと右下にポップアップで「Hello World」と表示される。

実装を読む

main 文に該当するのは src/extension.ts ファイルの activate() 関数。
package.json の activationEvents に記載のタイミングで実行される。
逆に終了時は deactivate() が呼ばれるとのこと。

'use strict';
import * as vscode from 'vscode';

// これが main文
export function activate(context: vscode.ExtensionContext) {
    // ログ出力。実行されたらデバッグコンソールに出力される。
    // ※ここは一度だけ通る
    console.log('Congratulations, your extension "test" is now active!');

    // 'extension.sayHello' はコマンドの名前。
    // ※ package.json にも記述する
    let disposable = vscode.commands.registerCommand('extension.sayHello', () => {
        // ※このスコープはコマンドを実行する度に通る
        // 右下に表示されたインフォーメーションメッセージはこれ
        vscode.window.showInformationMessage('Hello World!');
    });
    context.subscriptions.push(disposable);
}

// 終了時に呼ばれるデストラクタみたいなもの
export function deactivate() {
}

package.json を読む

実装と紐づいている箇所としては、activationEvents あたり。

{
    "name": "test", ←★yo code 時に入力した内容
    "displayName": "test",←★yo code 時に入力した内容
    "description": "test",←★yo code 時に入力した内容
    "version": "0.0.1",
    "engines": {
        "vscode": "^1.29.0"
    },
    "categories": [
        "Other"
    ],
    "activationEvents": [
        "onCommand:extension.sayHello"
        ↑★vscode.commands.registerCommand() の第1引数と同じ
    ],
    "main": "./out/extension",
    "contributes": {
        "commands": [
            {
                "command": "extension.sayHello",
                ↑★vscode.commands.registerCommand() の第1引数と同じ
                "title": "Hello World"
                ↑★コマンドパレットで入力するのはこっち
            }
        ]
    },
   :
}

VSCodeでLSP(クライアント側のサンプルを読む編)

https://code.visualstudio.com/docs/extensions/example-language-server
のクライアント側のコードを読んで理解してみる。
クライアント側は vscode-extension-samples/lsp-sample/client 配下のファイル。

client/package.json

まずは activationEvents。

"activationEvents": [
    "onLanguage:plaintext"
]

activationEvents は onLanguage に記載されたファイルが開かれる度に、拡張機能(LSPのクライアント)が実行される。

"configuration": {
    "type": "object",
    "title": "Example configuration",
    "properties": {
        "lspSample.maxNumberOfProblems": {
            "scope": "resource",
            "type": "number",
            "default": 100,
            "description": "Controls the maximum number of problems produced by the server."
        }
    }
}

「この設定は起動時や、設定が変更されるたびに言語サーバーに送信される」とのこと。

"dependencies": {
    "vscode": "^1.1.18",
    "vscode-languageclient": "^4.1.4"
}

vscode extension host APIvscode-languageclient library のバージョン。

client/src/extension.ts

クライアント側のコードのエントリポイント。
synchronize のあたりがイマイチ理解できていない。

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient';

export function activate(context: ExtensionContext) {
        // サーバー側の起動設定
        // ...本サンプルではサーバー側が TypeScript で実装されているので
        // ...TypeScript からコンパイルした JS のソース位置を指定している模様
	let serverModule = context.asAbsolutePath(path.join('server', 'server.js'));
	// ...デバッグ時のオプション指定
        // ......--inspect はなんだろ
	let debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] };
	
	// ...デバッグモードの時は debug, それ以外は run
	let serverOptions: ServerOptions = {
		run : { module: serverModule, transport: TransportKind.ipc },
		debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
	}
	
	// クライアント側のオプション
	let clientOptions: LanguageClientOptions = {
		// ...テキストファイルを対象とする
		documentSelector: [{scheme: 'file', language: 'plaintext'}],
		synchronize: {
			// ...以下の package.json の configuration.properties の
                        // ...lspSample をサーバーと同期する。
                        // ...(lspSample.maxNumberOfProblems の値の変更がサーバーにも反映される?)
			// ......"configuration": {
 			// ......   "properties": {
			// ......        "lspSample.maxNumberOfProblems": 
			configurationSection: 'lspSample',
			// ... ワークスペース内の .clientrc ファイルのの変更をサーバーに通知する
			fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
		}
	}
	
	// クライアント側の開始
	let disposable = new LanguageClient('lspSample', 'Language Server Example', serverOptions, clientOptions).start();
	
	// クライアントが拡張機能停止時に非アクティブ化できるようにする
	context.subscriptions.push(disposable);
}

サーバー側の実装が TypeScript 以外の場合の設定方法

Launguage Server Protocol のサーバー側の実装はどんな言語でもよいとなっている。
サンプルでは TypeScript でサーバーが実装されているが、それ以外の言語で実装した場合は、
クライアント側はどう記述すればよいのか調べてみた。

TypeScript

まずは、サンプルのTypeScript。
以下の部分がポイントのよう。

	let serverModule = context.asAbsolutePath(path.join('server', 'server.js'));
	let serverOptions: ServerOptions = {
		run : { module: serverModule, transport: TransportKind.ipc },
		debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
	}
C#
  • dll

OmniSharp から拝借
https://github.com/OmniSharp/csharp-language-server-protocol/blob/master/vscode-testextension/src/extension.ts
サーバーを dll で作成して 'dotnet' コマンドで起動する模様。
ServerOptions.run(debug).module の代わりに command を使う

    let serverExe = 'dotnet';
    let serverOptions: ServerOptions = {
        run: { command: serverExe, args: ['D:\\Development\\Omnisharp\\csharp-language-server-protocol\\sample\\SampleServer\\bin\\Debug\\netcoreapp2.0\\win7-x64\\SampleServer.dll', '-lsp'] },
        debug: { command: serverExe, args: ['D:\\Development\\Omnisharp\\csharp-language-server-protocol\\sample\\SampleServer\\bin\\Debug\\netcoreapp2.0\\win7-x64\\SampleServer.dll', '-lsp'] }
    }
  • exe

https://github.com/matarillo/vscode-languageserver-csharp-example/blob/master/client/src/extension.ts
から拝借。
こちらは exe を起動している。

	let serverCommand = context.asAbsolutePath(path.join('server', 'SampleServer.exe'));
	let commandOptions: ExecutableOptions = { stdio: 'pipe', detached: false };
	let serverOptions: ServerOptions =
		(os.platform() === 'win32') ? {
			run : <Executable>{ command: serverCommand, options: commandOptions },
			debug: <Executable>{ command: serverCommand, options: commandOptions }
		} : {
			run : <Executable>{ command: 'mono', args: [serverCommand], options: commandOptions },
			debug: <Executable>{ command: 'mono', args: [serverCommand], options: commandOptions }
		};
Java

qiitaの記事LSP4JでLanguage Server Protocol入門から拝借
command に java のパス、args に -jar と jar の場所を指定している。

    const serverOptions: Executable = {
        command: "C:/PROGRA~1/Zulu/zulu-10/bin/java",
        args: ["-jar", "F:/work/TeachYourselfLsp4j/server/build/libs/TeachYourselfLsp4j-1.0-SNAPSHOT.jar"]
    }

西日本豪雨のボランティアに参加してきました

岡山県真備町西日本豪雨のボランティアに参加してきました。
今回の西日本豪雨のボランティアを考えている方への情報共有になれば。

私のスペック

  • ボランティアは初めて
  • 東京在住
  • 運動は週一回ジムに行くぐらい
  • 一日中パソコンに向かう仕事

今回の工程

  • 前日に東京から夜行バス(3列シート)に乗り、朝 7 時に倉敷到着。
  • 他のボランティアの方の車に相乗りさせてもらい、岡山県真備町
  • 9時〜18時頃まで作業

復興支援の団体

岡山県での復興支援で見つけたのは以下の2つがありました。

どちらも 9:00 からなので、東京から参加する場合は当日電車では間に合いません。
前泊するか深夜バスで行くことになります。
なお、深夜バスの場合は渋滞などで遅延する場合もありますのでご注意ください。

作業について

浸水被害にあったお宅の家財を外に出し、それをトラックで集積上に運ぶのを繰り返します。
初めてのボランティアだったので緊張していましたが難しい作業はなかったのでこなすことができました。


ボランティアは「自己責任・自分で考えて行動する」っと言われています。
っで、これが「自分なんかが行って役に立てるかなぁ」っと二の足を踏む理由なのかなと思います。
でも少し柔らかい言い方に変えると「自分のできることを、やれる範囲でやってみる」ってことだと思います。
体力がない方、力に自信がない方でも、やり方によってできる作業はいくらでもあります。
例えば、大きい家財道具は力のある人に任せて、小物をごみ袋に詰める作業を中心にやるとか、
極力日陰に入って作業するとか、休憩を多めにとるなどです。


作業するうえでの留意点としては以下のサイトが参考になります

作業道具について

ポイントは「ゴム手袋・長靴・マスク」です。
水害なので家財道具がかなり濡れています。またガラスの破片や釘なんかもあります。
ゴム手袋といってもホームセンターで売っている厚手の"耐油" と書かれた手袋が安全です。
ちなみに私が使ったのはこちらです。
http://www.amazon.co.jp/dp/B000TGJGNW
手の甲側が布になっているタイプもありますが、それだと水がしみるので
全面ビニールっぽい生地がいいです。家庭用のゴム手袋や軍手では危ないです。


同じ理由で靴も長靴がベストです。水場に入らないのであれば登山靴のような厚手の靴でもいいと思います。


ホコリがすごいのと、匂いがすることがあるので、マスクも必須です。
マスクはコンビニとかで売っているマスクを使いました。
建築現場で使われているような立体的な防塵マスクがあればなお良しですが、なかなか売ってないのでコンビニのものでいいと思います。。

熱中症について

これが一番の大敵でした…。
絶対に無理しちゃだめです。
無理してなくても作業に乗ってくるとやりすぎてしまって、いつの間にかバテてる何てこともあります。
「のどが渇く前に水分を取る。」
「他の人が作業していても自分のペースで休む。」
とにかく休憩が大事だと思います。なんなら、作業時間よりも休憩時間の方が多いぐらいでいいかもしれません。
それぐらい今年の暑さは厳しいです…。
水分は最低でも 2L は用意してください。
もちろん帽子は必須です。
自分は夜行バスで倉敷まで行きましたが「夜行バスでは寝れない」という人は前日入りしてホテルなどで十分に睡眠をとることをお勧めします。

ボランティア保険について

基本、ボランティア保険への加入が必須となっていると思います。これについて少し注意が必要です。
これは以下のサイトが参考になります。
https://insurance.yahoo.co.jp/volunteer/index.html

ポイントは以下です。

  • 基本は自分の住んでいる自治体の社会福祉協議会で加入。大規模災害時は災害のあった場所でも加入できるが、混雑することがあるので、避けた方がよい。
  • 社会福祉協議会は役所とは別の場所にあることがある。
  • インターネットやFAXでの加入ができない
  • 種類として、天災型と普通のものがあるが、今回は天災型での加入が必要。一番安いので来年3月末まてで600円。

役所と違う場所にあることがあるのが面倒です。私は住んでる自治体の役所の出張所に電話してボランティアセンター(社会福祉協議会)の場所を確認しました。

所感

2018/07/19 現在ですが全然町は片付いていません。人手がとにかく必要だと思いました。
今回作業させてもらったお宅を片付けるだけでも 平均12人前後いても 5 日はかかりそうでした。
岡山では床上浸水以上の被害が 4280件あったそうです。仮にその 1/4 で片付けの支援が必要だとすると 64,200 人日かかることになります(超単純計算なので精度は曖昧です(フェルミ推定ムズイ…))。
っで、これは岡山だけの数字なので、広島や他の地域を含めるともっと多くなります。
「交通費をかけて現地にいくぐらいなら、そのお金を義援金に入れた方が役に立つのでは?」という考え方もあるみたいです。ある程度町が片付いた後、町を立て直していく段階ではこの考え方も有効なのかもしれません。
でも、今の災害ごみの片づけというフェーズでは人手の方が必要なように思いました。


時間があって何か支援をしたいと思っているけど、「自分なんかが行って役に立てるかなぁ」と迷っている方には、
とにかく行ってみることをお勧めします。
上記の準備物やら作業上の注意点やら熱中症に配慮してさえいれば、他は難しいことはないですし、
何かしらできることはあると思います。


最後になりましたが、一日も早い復興を心からお祈りします。

VSCodeでLSP(サンプルを動かす編)

Visual Studio Code で Launguage Server Protocol を作る手始めとして、以下のページに書かれてあるサンプルを動かしてみた。

Language Server Extension Guide | Visual Studio Code Extension API

  1. github からリポジトリをクローンして、lsp-sample フォルダで VSCode を起動

> git clone https://github.com/Microsoft/vscode-extension-samples.git
> cd lsp-sample
> npm install
> code .

  1. ビルドタスクの実行(ショートカットならCtrl+Shift+B)
  2. デバッグビューを開いて launch configuration を "Launch Client" を選択し、"デバッグの開始" ボタンを押下する。→ VSCode がもう一つ立ち上がる。立ち上がったVSCodeのことを"拡張機能開発ホスト(Extension Development Host)"というらしい

  1. 以下を記載したファイルをtext.txtという名前で保存

typescript lets you write JavaScript the way you really want to.
typescript is a typed superset of JavaScript that compiles to plain JavaScript.
Any browser. Any host. Any OS. Open Source.

  • サーバー側のデバッグ
    • launch configuration を "Launch Client" にして起動しただけでは、サーバー側のデバッグができないので以下の操作が必要。
    1. launch configuration を "Launch Client"で起動
    2. 拡張機能開発ホストが起動した後、launch configuration を "Attach to Server" にしてデバッグを開始(※なぜか Attach to Server の時は F5 が効かないのでメニューかボタンで実行する)。成功すると以下のような画面になる。

Pattern 1. Replacing Functional Interface

"Functional Programming Patterns in Scala and Clojure"
という本を読んでいます。
内容は「オブジェクト指向のコードを関数型プログラムに置き換えるとどうなるか」
というのを、パターン形式で記述してあります。
英語は苦手だし Java は大学以降やっていないので内容を正しく把握できていないところもあるかもですが、内容をまとめてみたいと思います。
Clojure まで手を付けると読み終わらなさそうなので Scala だけを(^^;)

1 つ目のパターンは Replacing Functional Interface。訳すと「関数型インターフェースの置き換え」。

前提知識

Functional Interface とは

メソッドが 1 つだけ定義された interface。
Runnable インターフェース、Callable インターフェースなど。
wikipedia:関数オブジェクト の "Java における関数オブジェクト" が詳しい。

FP(Scala) だとどう変わるか

  • Functional Interface が不要になる。
  • Functional Interface のクライアントのコードが短くなる。
    • Functional Interface のクライアントコードは下記のような実装がよくされる
      • 1. 匿名クラスを作成
      • 2. 抽象メソッドを宣言
      • 3. 実装
    • これらのうち、1.、2. が不要になる
    • 例A
Collections.sort(hoge, new Comparator() {  ←★ 1.
    public int compare(Hoge h1, Hoge h2) {       ←★ 2.
        return h1.getValue() < h2.getValue();    ←★ 3.
    }
});

どういう方法か

  • 高階関数を使う
    • Functional Interface の代わりに関数を渡す。例A は下記のように書き換わる(イメージ)
Collections.sort(hoge, (h1, h2) => return h1.getId().compareTo(h2.getId()))
↑ 第 2 引数に "引数が 2 つで戻り値が boolean の関数" を渡す
↑ あくまでイメージなのでコンパイル通りません...
    • ラムダ式を名前付き関数にしてみると次のようになる
def complicatedSort(h1: Hoge, h2: Hoge) = h1.getValue() > h2.getValue()
Collections.sort(hoge, complicatedSort)
↑ Java だと、Comparator インターフェースを implements したクラスを作成して、compare メソッドを実装する必要があるが、
↑ 高階関数だと適切な引数と戻り値を持ったメソッドを定義するだけでよい。

OOP と FP の比較

常に匿名クラス、無名関数で実装するなら、Functional Interface にする意味はあまりなさそう。
名前付きクラス、名前付き関数の場合、高階関数で実装すると、引数と戻り値が同じだったらどんな関数でも渡せるという柔軟性がある。
一方で、Functional Interface を使う方が実装の意図は伝わりやすいかもしれない。Comparator を implements してたら比較用のメソッドと分かるし、メソッド名も必ず compare にしないといけないと制限できる。
クライアント側に制約をかけるという目的があるなら Functional Interface を使うのもありかもしれない。あんまりそういうケースはない気がするけど。

ストラウストラップのプログラミング入門:組み込みシステムプログラミング

ストラウストラップのプログラミング入門を買って読んでる。
25章の「組み込みシステムプログラミング」で C++ で組み込みシステムを実装するときの注意点がまとまっていて、どうしても読みたかったので買ったのだけど…1000ページ超えってぶ厚すぎ!
電子書籍で出して欲しかったなぁ。

気になったところをまとめてみる。

予測可能性

予測可能性は「ある処理にかかる時間が一定であるかどうか」を指す。
ハードリアルタイムを実現するためにはこれが大事。
例えば、要素数が分からないリストの線形探索にかかる時間は予測可能ではない。
ハードリアルタイムに対応する為に避けなければいけない実装として下記が挙げられている。

  • new と delete を使ったフリーストア割り当て
  • 例外
  • dynamic_cast
メモリ管理(new と delete を使ったフリーストア割り当て)

プログラムの実行時に問題となるメモリは「スタックメモリ」と「ヒープメモリ」。

  • 「スタックメモリ」はプログラムの関数呼び出しのネストが許容範囲内に収まるようにすることが必要。この関係上再帰呼び出しを禁止にする、もしくは再帰の代わりに反復(反復は末尾再帰と同義??)を使用するといった対処が必要。
  • new が予測可能でないのは、メモリの断片化が発生するため。断片化が発生するとちょうどよいサイズのメモリを探す時間が長くなるため new にかかる時間が長くなる。
    • 対策としては、 スタックを使用する、オブジェクトをプールするという方法がある。
    • スタックだとサイズが異なるオブジェクトの割り当てができる。プールだとできない。
    • STL のコンテナ、string は間接的に new を使用するため使用しない。
例外

各 throw に対してどの catch が呼び出されるか、throuw がそこに到達するまでにどれくらいかかるかが、予測可能性に関わる。

dynamic_cast

根本的な問題ではないとだけ書かれてある。なんのこっちゃ?

アドレス、ポインタ、配列

組み込みシステムは信頼性が重要。そのために、ポインタの取り扱いに注意することとして挙げられている

チェックされない変換

明示的な変換は使用しないこと。
ただし、下記のようにデバイスドライバへのアクセスなどでやむを得ない場合はある。

Device_driver* p = reinterpret_cast<Device_driver*>(0xffb8);

※ reinterpret_cast は関連のない型の間の変換を行う。

正しく機能しないインターフェイス

Shape の子クラス Circle が定義されているときに下記の実装は問題を起こす

void poor (Shape* p, int sz) {  // Shape* p は Shape の配列を意図している
  for (int i = 0; i < sz; ++i) p[i].draw();
}
void main() {
  vector<Circle> circles;
  …circles にいくつかデータを詰める処理が入ったとして… 
  poor(&circles[0], circles.size());
}

Circle は子クラスなので Shape よりもメモリを多く使用する。
しかし、poor() 内の for 文のポインタ p は sizeof(Share) ずつずれて行くため、プログラムがクラッシュする
(p の実体が Circle だと sizeof(Circle) になっていない上手くいかない)
実は、Circle で属性が追加で実装されていなければ、サイズは同じになるので上記の実装でも上手くいく。でも、後から Circle に属性が追加になったなんてことになると、見つけにくいバグになる。

Shape* と Circle* は暗黙の型変換が働くので、コンパイルエラーにならない。
下記のように書き換えると大丈夫。
(C と C は成立しないため(Cはパラメータ化されたクラス))

void general (vector<Shape>&);

でも、組み込みだとこの対策は使えない場合がある。フリーストアの問題があるので vector が使用できないため。

コンテナを使わないで上記の問題を解決方法としてインターフェイスクラスが載っている

  • インターフェースクラス
template<class T>
class Array_ref {
public:
  Array_ref (T* pp, int s) : p(pp), sz(s) { }
  
  T& operator [ ](int n) { return p[n];}
  const T& operator[ ](int n) const { return p[n];}

  bool assign(Array_ref a) {
    if (a.sz != sz) return false;
    for (int i = 0; i < sz; ++i) { p[i] = a.p[i]; }
    return true;
  }
  void reset (Array_ref a) { reset (a.p, a.sz);}
  void reset (T* pp, int s) { p = pp; sz = s;}
  inst size () const { return sz;}

private :
  T* p;
  int sz;
};

要するにフリーストアしないコンテナを自作しようということ。
これで、型の問題は解決する。

void better (Array_ref<Shape> p) {
  for (int i = 0; i < sz; ++i) p[i].draw();
}
void main() {
  Array_ref<Circle> circles;
  …circles にいくつかデータを詰める処理が入ったとして… 
  better(circles);  // エラーになる
}
    • 余談1

Array_ref が Array_ref に変換できない理由をまとめてみる。
・Shape に calcArea() という関数が定義されている
・Circle が「半径」という属性を持っている(Shape には「半径」はない)
・calcArea() をオーバーライドして「半径」を参照した処理が書かれてある
という状況で。

Shape s;
Circle c;
s = c;
s.calcArea();

s = c とした時に、サイズが合わないので c の属性「半径」が切り落とされる。その状態で s.calcArea() を実行すると「半径」を参照できないのでクラッシュ(もしくは動作不定)する。そのため、Array_ref を Array_ref に代入できない。

    • 余談2

では、Array_ref が Array_ref だとどうかというと、これもだめ。その例は以下。

Array_ref<Shape*> shapes;
Array_ref<Circle*> circles;
shapes = circles;
shapes[3] = new Rectangle(); // Rectangle は Shape の子クラス。Circle とは別階層

shapes に circles を代入したところで、shapes の実体は Array_ref。そこに Circle と親子関係にない Rectangle を代入すると訳が分からないことになる。

  • 継承とコンテナ

【インターフェースクラス】の例で、危険な代入は防げるようになった。
でも、ポリモーフィズムに対処できない。
かといって、【余談2】で書いたように Array_ref, Array_ref も代入できない。
これに対処する方法も書かれてあったが、書いてるときりがないので省略

その他

ビット演算とか、コーディング標準とかの話が書かれてあった。