igsr5 のブログ

呟きたい時に呟きます

自分のパフォーマンスを最大化し続ける方法

こんにちは。個人パフォーマンスを安定させることが得意な @igsr5_ です。

最近何人かに何か工夫してることあるの?と聞かれたのでブログとして書き出してみようと思います。

はじめに

業務に慣れてくると、求められるアウトプットのレベルや量は上がっていきます。

しかし業務知識をいくら習得しても、働き方が非効率だとパフォーマンスは安定せず、上昇する期待値についていけません。 仕事の成果を増やしたり、残業を減らしてプライベート時間を充実させるためには自分のパフォーマンスを最大化し続ける必要があります。

4つの意識を持つ

自分のパフォーマンスを最大化し続けるためには、4つの意識を持ちましょう。

  1. 決して「余裕」を失わない
  2. 「よし、やるぞ!」と脳が勝手に思える習慣を持つ
  3. 仕事に「モヤモヤ」を持ちこまない
  4. 健康でなければ全ての工夫は無意味

重要なのはプロジェクトが佳境の時など、普段より忙しい時こそ4つの意識を忘れないことです。ダラダラ仕事をしても満足な品質は得られないので、決して稼働時間を増やして解決しようなどと思ってはいけません

自分の場合

以下は自分が最近忙しかった週のカレンダーです。

※ 19時以降の予定は仕事とは関係ない予定です。予定名は塗りつぶしています。

先週の自分のカレンダー

この週はメインプロジェクトが佳境でタスクが無限にある状態で、サブプロジェクトの進行と長期インターン生のメンター業務を進めつつ、午後は大体 MTG で埋まっていて(マネージャーではありません)、しかも月曜は祝日というハードな週でした。

しかし前述の4つの意識を実践したところ、予定通りのスケジュールかつ満足のいく品質で全タスクを完了させることが出来ました。

実行方法

4つの意識をどう実行するかは人による部分があると思います。 ここでは自分が日常的に実践している方法を紹介します。

決して「余裕」を失わない

余裕を持つことで、仕事の質が上がったり、空いた時間を有効活用できたり、突然のアクシデントにも強くなったりします。余裕を持つために自分は以下の工夫を行なっています。

  • ロケットスタートで行う
    • その週のタスクは月曜に8割終わらせる
    • その日のタスクは午前に8割終わらせる
  • 計画的に行う
    • デイリーのタスク管理、今後リスクになりそうな問題の先回りなど
    • 午前中にロケットスタートを決めるため、前日までに計画を済ませておくと良い
  • 集中したい時間帯に MTG を入れない
    • ~12:00

「よし、やるぞ!」と脳が勝手に思える習慣を持つ

フロー状態に簡単に入るスイッチを持っておくと良いです。自分の場合は以下の条件がスイッチです。

  • 午前中
  • イヤホン・ディスプレイ・椅子と机を用意する
  • iPhone を集中モードにする
  • 私服に着替える

逆にどうしてもやる気が出ない時はこれらの行動は避けています。これは「条件達成 = フロー状態に入る」の信用が崩れないようにするためです。

仕事に「モヤモヤ」を持ちこまない

頭の中のモヤモヤは集中を妨げる原因になります。モヤモヤしながら仕事をしても上手くいかず、さらにモヤモヤする悪循環に陥ります。

しかしモヤモヤを完全に消すことは出来ないので、自分は以下の方法でモヤモヤとうまく付き合っています。

  • いつでも開ける日記を持つ
    • モヤモヤや考え事は思いついたタイミングで書くことで頭から一度追い出す
  • 気分がノらない時は何もしない
    • 気分がノらない時の3時間は気分がノっている時の30分

健康でなければ全ての工夫は無意味

例えば風邪をひいてしまったり、夜更かしして日中眠くなってしまうとどうしても集中力は生まれません。 逆に体が健康であれば、それだけで集中力が大きく向上するので働く上ではなるべく健康が良いです。

自分は健康のために以下のような方法をとっています。

  • 睡眠を整える
    • 寝つきと寝起きを良くする
    • 睡眠途中で目が覚めないようにする
  • 週3でランニングする
    • 定期的に汗を流すことで幸せホルモン的なやつが溢れ出る(気がする)
  • 17時退勤を目指す
    • ダラダラと残業すると疲れが取れにくい

まとめ

自分のパフォーマンスを最大化し続けるためには、4つの意識を持ちましょう。

  1. 決して「余裕」を失わない
  2. 「よし、やるぞ!」と脳が勝手に思える習慣を持つ
  3. 仕事に「モヤモヤ」を持ちこまない
  4. 健康でなければ全ての工夫は無意味

重要なのはプロジェクトが佳境の時など、普段より忙しい時こそ4つの意識を忘れないことです。ダラダラ仕事をしても満足な品質は得られないので、決して稼働時間を増やして解決しようなどと思ってはいけません

GitHub Projects (v2) で個人の Issue/PR 管理をエンジニアリングする

先日、GitHub Projects (v2) を利用して個人の Issue/PR 管理を良い感じに自動化したのでその時の話をします。

ことのきっかけ

一般的なプラクティスかどうかは分かりませんがこの世の中には、個人専用の GitHub Projects を作り、そこで自身の Issue や PR を追加して管理している人たちがいます。

進行中のプロジェクト、突如発生するサブタスクなどに付随して、関わる Issue/PR の数はどんどん肥大化していきます。そんな問題に対して GitHub Projects による Issue/PR 管理はは人間の認知負荷を下げることに役立ちます。また筆者も 1 年ほど前から GitHub Projects で Issue/PR を管理しています。

しかしながら、実際に 1 年ほど運用してみると、今度は GitHub Projects 運用の複雑化が目立つようになってしまいました。例えば GitHub Projects への Issue/PR の追加漏れやステータス更新の手間、フィールド数の肥大化などの問題が度々見られていました。散乱する Issue/PR たちの辛みに比べればまだマシですが、やはり微妙感は漂います。

そこで「このまま GitHub Projects を腐らせておくのは勿体無い!」という気持ちのもと、個人用 GitHub Projects のリファクタリングを実施することにしました。

前提: Projects で管理したい Issue/PR

次の章に進む前に、自分が GitHub Projects で管理したいと思っている Issue/PR の種別を示します。Why には踏み込みませんが、今回の取り組みではこれらの Issue/PR 管理が生産性高く行える形を目指します。

  • 作業中の Issue
  • 作業中の PR
    • 実装中の PR
    • レビュー待ちの PR
    • Approve された PR
  • レビュー中の PR
    • 未レビューの PR
    • レビューを返した PR
    • Approve 済みの PR
  • 完了済みの Issue/PR

やったこと

ある程度の問題整理をしたのち、今回の取り組みでは以下の問題の解決にあたることにしました。

  1. 運用の認知負荷を下げるために、不要なステータスやカスタムフィールドを整理したい
  2. レビュー漏れ等のリスクを防ぐために、Projects への追加漏れ・ステータス更新漏れの発生を防ぎたい
  3. 継続的な運用のために、限りなく Projects の操作 (mutation) を自動化したい
  4. 継続的な運用のために、この先アイテム上限数 (1200) に達しないようする

それぞれ具体的にやることは以下の通りです。

No. やること
1 フィールド設計
2, 3 Projects 操作の自動化
4 Archiving items の利用

フィールド設計

まずは「前提: Projects で管理したい Issue/PR」たちをどのように分類するかを考えます。

  • 作業中の Issue
  • 作業中の PR
    • 実装中の PR
    • レビュー待ちの PR
    • Approve された PR
  • レビュー中の PR
    • 未レビューの PR
    • レビューを返した PR
    • Approve 済みの PR
  • 完了済みの Issue/PR

つまりフィールド設計ですが、アイテムの状態 8 個をそのままフラットに扱うメリットはなかったので、今回は "カテゴリー" と "ステータス" という二つの概念を導入してその積でアイテムの状態を決定することとします。

用語 定義
カテゴリー アイテム(Issue/PR)が属する大まかな分類。作業の性質に基づいて分けられる。
ステータス アイテム(Issue/PR)の進行状況を示す。
  • カテゴリー
    • Issues: 作業中の Issue
    • PRs: 作業中の PR
    • Review PRs:レビューリクエストを受けた PR
  • ステータス
    • In Progress: 何かしらの作業中 (未着手を含む)
    • In Pending: 何かしらの待ち状態
    • Complete: 完了可能になった状態

これらのフィールドを用いて、アイテムの状態は以下のように考えます。

カテゴリー(下) x ステータス(上) Issues PRs Review PRs
In Progress 作業中の Issue 作業中の PR 未レビューの PR
In Pending レビュー待ちの PR 未レビューの PR
Complete Approve 済みの PR Approve 済みの PR

※ は今回は利用しない状態をさします。なお「完了済みの Issue/PR」については問題 4 (アイテム上限数) の考慮を行うために別対応を行うのでここでは除外しています。

完了済みアイテムについて

GitHub Projects には追加できるアイテム数に上限があります (2023/10 時点で 1200 個)。業務で普段使いしていると大体半年~1 年で上限に達してしまうので継続的に Projects を利用していくためには不要になったアイテムから棚卸をしないといけません。

とはいえ完全に GitHub Projects から削除してしまうと後に掘り起こす際に不便なので、今回は Delete ではなく Archive を利用することで対応しました。Archive にしておけば専用の Archive Item ページから Issue/PR を検索することも出来るので便利です。

参考: Archiving items automatically - GitHub Docs

自動化

状態遷移図

自動化にあたり、まずはフィールド設計で導いた "カテゴリー" × "ステータス" の 8 状態 +α の状態遷移を整理しました。以下がその結果です。 ここでは前章で挙げた 8 状態に加えて「Projects 未追加」「Archive 予定 (1 日後に Archive されるアイテム)」の 2 状態を考慮しています。

Issues/PR の状態遷移図

こうして見ると全ての状態遷移は何かしらの機械判定が可能なものになっています。機械判定可能ということは自動化も可能であると推測出来るので、ひとまずはこの状態遷移図を主軸として自動化を試ることにします。

実装

GitHub Projects には公式が用意するワークフロー制御の機構が存在しますが、残念ながら今回実現したい自動化の要件を満たす機能は Auto Archiving を除くと他に 1 つもありません。さらに次点で有力そうな Webhook を活用する方法もまた、自身が管理ユーザーではない Organization に対する Webhook URL 登録が出来なかったため見送っています。

ここまで来ると、もはや残された道は GitHub API を利用して 0 から自分で状態遷移を実装を作ることくらいしかありません。こればかりは現状だとどうしようもないので、今回は自身で今回の要件を満たすステートマシンを実装することにしました。なおステートレスに実装できないか PoC を作ってみましたが、流石に状態・状態遷移の数が多くて複雑だったため諦めてステートマシンの実装を行うことにしました。

実装の詳細についてはそれ単体で文量が爆発してしまうためここでは割愛させていただきます。

作ってみて

執筆時点では PoC 段階が終わったフェーズでまだ本実装には進んでいませんが、PoC の成果物もそれなりに活用可能な出来にはなったので実際に業務で利用することにしました。実際に使用してみると全ての Issue/PR が自動で管理される世界はとても快適でした。

また、今回の取り組みによって今まで増え続けていたいくつものフィールドが消えて、"カテゴリー" と "ステータス" のみシンプルな構造になりました。フィールド設計がシンプルに保てているのでボードを作成する際にも条件の構築が行いやすく体験がよかったです。

さらに、これは考えていなかったのですが、いざ自動化実装を動かしてみると、自分が放置していた大量の過去 Issue/PR たちが掘り起こされました。これらを特別に非表示にするオプションは導入していない (導入する予定もない) ので、プロジェクトの利便性を上げるためには必然的に不要になった Issue/PR を close することになりましたが、これがめちゃくちゃ良かったです。軽く棚卸しただけでも 50 個弱の Issue/PR が close 出来たので驚いています (反省)。

おまけ

  • 自動化は Cloud Run と Cloud Scheduler を使っています
    • 平日の9時-18時のあいだ、3分毎に GitHub API V3 を叩いて GitHub Projects の状態を更新しています
    • ロジック自体は Golang で記述
    • GitHub Api V3 の search api の rate limit に引っかからないように頻度を調整しています
  • 節約 月2,000円 → 月750円
    • 初めは時間帯制限を設けず平日の24時間フルで Cloud Scheduler を回していましたが、Cloud Run の CPU Allocation Time が増えすぎて月 2,000 円ほどかかっていたので実行タイミングを減らして節約しました。

2023年の振り返り

2023年の振り返りをします。昨年の振り返りはこちら。

igsr5.hatenablog.com

大忙しで楽しい一年でした

一瞬で過ぎたように感じつつ、振り返ればたった365日とは思えない重厚な一年になりました。

仕事・成長

2021年8月から長期インターンシップとして働いていたウォンテッドリー株式会社に正社員として入社しました。入社時点で既に在籍期間が2年弱になっていたので正直何も変わらないかな?と思っていましたが、予想に反して自分の考え方や動き方が変わったことに驚きました。

その後4月から11月までは基盤開発チームとしてプロダクト仕様の大幅変更に伴うリファクタリング及び新機能開発に従事しました。私は主にバックエンド開発とプロジェクトマネジメントを担当していました。 これが中々に高難易度なプロジェクトだったので、この半年で自身のバックエンドスキルやプロジェクトマネジメントスキルの成長に大きく繋がったと感じています。また結果的にスケジュール通りかつ高品質にプロジェクトを終えることができたのですが、これほどにレベルの高いチーム開発は今まで味わったことがなかったのでとても良い経験になりました。当時のチームメンバーと後に「あの時楽しかったねー」といった会話をすることがあるくらい良いプロジェクトだったなと思います。

12月からはチームを異動し、全く別の新規プロダクト開発にバックエンドエンジニアとして参画していました。このブログを書いている時点ではまだ何も公開できる情報がないので詳細は伏せますが、少人数のチームなこともありバックエンド領域の設計や実装は完全に自分が責任を持ってリードしています。今までよりも高いレイヤー*1でのバックエンド設計や意思決定が求められる立場で楽しく仕事ができています。

またメインタスクの側、8月には DevOps 専門チームの立ち上げを行ないました。立ち上げといってもそんなに大したことはしておらず自分の考えていた「こういうチーム作りたい!」を社内プレゼンを通して主張したり草の根的に分科会を立ち上げたりしていたらいつの間にか正式なチームになっていました。まだまだ走り出したばかりのチームで、しかも未熟な自分がリードしているチームなので伸び代だらけですが、入社前からずっとやりたかったことなのでとても楽しく活動できています。何より協力してくれているメンバーに感謝です。チームの立ち上げ話は12月の外部登壇で詳しく話したのでその際の Speaker Deck を置いておきます。

speakerdeck.com

引っ越し・同棲

就職を機に実家のある愛知県から東京都に引っ越しました。現在は目黒駅すぐ近くに住んでいるのですが、好立地かつオフィスも近いのでかなり便利です。

また東京に来てしばらくした後、付き合っている彼女と同棲を始めました。計画していた同棲ではなくある日突然スタートした同棲 (!?) だったのですが、そんな始まりにも関わらず限りなく安定した生活を送れています。

生活スタイルとしては基本的に一人の時間は少なく常に2人で何かしらするというスタイルです。自分の性格的にたまに独りの時間も欲しくなるのですが、それは仕事の時間でいい塩梅に調整できているので特に不満はなく楽しく暮らしています。彼女が非常に活発な性格なので休日や仕事後はよく旅行や飲みに連れて行ってもらいました。私自身は怠惰な性格なので家を出る前は軽くゴネながらソファにしがみついているのですが、半強制的に外に連れ出してもらえているので良い息抜きになっています。また非常に陽気な彼女なので家でも常に変なことを言って叫んだり踊ったりしています。今も執筆しながら隣でよく分からないことを言っていますが楽しいです。

来年もこの生活が続くと良いなと願っています。感謝*2です。

下北沢のきれいな道

卒業・成人

今年の1月~3月は人生において学生最後の時間となりました。私は学業よりも仕事の方が何倍も楽しく感じる性格なので実はずっと早く就職したいと考えていたのですが、いざ就職してみると学生だった頃に戻りたくなる瞬間もあります。

特に学生最後の年は時間が有り余っていたので平日の深夜や休日はよく友人とプチ旅行に出掛けていました。滝を見に行こうと出掛けてみたり、昼過ぎまで友人宅で寝た後に大阪へ思いつきの弾丸旅行をしてみたり、平日ど真ん中の深夜にドライブをして謎に琵琶湖に到着してしまったりと思い返すととてつもなく楽しい時間だったなと思います。

東京に引っ越した後も何人かの友人とは定期的にご飯に行くのですが、それでも当時に比べると顔を合わせる頻度が減っており寂しく思います。仕事が忙しかったり同棲していたりするので必然ではあるものの、せめて来年以降も定期的な交流が途絶えることなく過ごしていきたいです。

1月には成人式がありました。正直そんなに感慨深くはなかったのですが単純に久しぶりの友人に会えて楽しかったです。何人と喋ったか覚えていませんが中学当時そこまで交友のなかった人ともなぜか話が盛り上がる不思議な空間でした。また同窓会とかで集まれると良いなと思います。

3月初めに行った神戸旅行

細かないろいろ

上で書ききれなかった細かいトピックを書いていきます。

仕事

プライベート

  • 彼女がシーシャが好きなので自宅用にシーシャの道具1式を揃えました。
    • それまでは週に2, 3回お店に通っていたので月60,000円ほどお金がかかっていたのですが、自宅で済ませられるようになってからは毎日シーシャを作ってもせいぜい1万円ほどで収まっているので幸せです。
  • 旅行にたくさんいきました
    • 夏に宮古島に1週間ほど滞在しました。海がきれいでたくさんの魚と泳ぎました。日焼けで動けなくなったのはいい思い出です。
  • 勉強もそれなりにしました。
  • 個人開発はあまり出来ていません。その代わり生産性への関心が強い年だったので休日に自身の開発環境やPC環境の整理を行ったりして業務スタイルのベストプラクティスを探索することが多かったです。

宮古島で泊まった部屋のベランダ

宮古島で泊まったホテルから見えた花火。この後向こう岸で火事になってた。

反省

楽しい一年だった反面、反省もあります。主な反省は以下の3つです。

  • お金
  • 健康診断
  • 睡眠不足

お金

毎月赤字でした。当然貯金もなく毎月の家賃の支払いが危ういレベルです。 また一緒にご飯に行った方にご迷惑をおかけしてしまうこともありました*3。本当に申し訳ないです、、

原因は単に使いすぎです。お金がないのに旅行に行ってしまったり外食してしまったりするからいけません。またそれらの支払いをクレカで済ませてしまうので毎月の支払額がどんどん上がっていきます。本質的には家計を管理する役割の不在が根本原因なので最近は自分が家計の管理をしていたりします。

最近は改善の兆しが見えてきているので来年は黒字を目指して計画的にいきたいです。

健康診断

9月の健康診断で少し怒られました。 お酒の飲み過ぎでコレステロール値が引っかかったのと1年前から体重が10キロ増えていました。

1年前までは健康など気にしたこともありませんでした。しかし最近は気づけば太る、体調を崩す、で恐ろしいです。 完全に運動不足とお酒の飲み過ぎが原因なので来年は善処します。

睡眠

2023年、というよりここ1、2ヶ月の話ですが、夜眠れません。また睡眠が満足に摂れていないので日中のパフォーマンスにも悪影響が出ています。

また原因がまだはっきり分かっておらず、よく分からないまま毎日 3, 4時まで寝れない日々が続いているので何とかしないとまずいです。ここでは特に何も書けませんが、来年はまずは睡眠の質を向上させようと思います。

来年の抱負

今年の良かったところを継続しつつ、来年はもっと楽しかったと言えるような一年にしたいです。

目標

  • 1月中に睡眠の問題を解決する
  • 2月までに家計を黒字にする
  • 9月の健康診断でコレステロール値・体重を前年より下げる
  • それ以外
    • 仕事で圧倒的成長する
    • 友人と定期的に会う
    • 同棲仲良く
    • プライベートで開発する・本読む

*1:1つのプロダクトを既存プロダクトとの整合を保ちながら全く新しくデザインするという意味

*2:しかも私は1回もキッチンに立ったことがありません。いつも美味しいご飯をありがとう。

*3:先輩に意図せず多く支払いをお願いしてしまったり、名古屋のゲイ日本酒バーで現金が用意できず店主の松子ちゃんにこっそり逃してもらったりしました、、

Shopify/ruby-lsp の内部実装を読み解く (3) v0.0.2 の変更箇所を途中まで

概要

個人的に最近アツい https://github.com/Shopify/ruby-lsp の内部実装を読み解いて追加機能の提案・実装が出来るレベルを目指す。

なおこの回以外のブログは https://d.hatena.ne.jp/keyword/reading-shopify-ruby-lsp#related-blog で見ることができる。

前提

筆者に LSP (Language Server Protocol) の知識はほとんどない。クライアントとして利用しているのでどういったものかは知っているが、その内部実装には明るくない。なのでこの記事を読んでいる人で内容の誤りに気がついたらコメントして頂けるとありがたい。

やること

https://github.com/Shopify/ruby-lsp のコミットを古い方から読んでいく。こういったコードリーディングはやったことないが、単に最新のソースコードを読むよりも Shopify の優秀なエンジニアの手癖を見ることが出来て学びが多そうなので実践してみる。

追記)分量が多くなってきたので第3回からはコミット単位ではなく、気になるPR単位で読む。

今回の内容

前回https://github.com/mtsmfm/language_server-protocol-ruby のコードリーディングを実施していた。今回は Shopify/ruby-lsp に戻ってコードリーディングを進めていく。

ここまでで v0.0.1 は見終わったので次は v0.0.2。

github.com

本編

Re-organize code to VSCode standards by mutecipher · Pull Request #2 · Shopify/ruby-lsp · GitHub

Ruby::Lsp::Handler クラスの実装が追加されている。このクラスは https://github.com/mtsmfm/language_server-protocol-ruby のサンプルコードをリファクタリングしたものになっている。変わらず JSON RPC のIOには標準入出力が利用されていて一瞬「あれ?」となったがどうやらこれが一般的らしい

通信は基本的に標準入出力を介して行われます。なので、両者のプロセスは完全に独立している訳ではなく、サーバーはクライアントの子プロセスとして起動されるのが普通です[1]。

実際のところ、LSPには通信方式についての規定はありません。標準入出力の代わりにTCPを使う例もあります。従ってサーバーが子プロセスにならないこともあり得ますが、実例に乏しいためこのように記述しています。

Remove VSCode client logic by mutecipher · Pull Request #6 · Shopify/ruby-lsp · GitHub

Shopify/ruby-lsp の内部実装を読み解く (1) v0.0.1 の変更箇所、LSP の基本動作、LSIF (Language Server Index Format) - igsr5 のブログ で「この時はまだ VSCode Extension (のちの Shopify/vscode-ruby-lsp)も同じリポジトリに同封されているのかー」と言っていたが #6 の PR でリポジトリを分けることが決まっていた。意外と早かった。

Add support for folding defs by RyanBrushett · Pull Request #10 · Shopify/ruby-lsp · GitHub

この PR では以下の 4 つの method の実装が行われている。

handler.config do
  on("initialize") do
    store.clear
    respond_with_capabilities
  end

  on("textDocument/didChange") do |request|
    uri = request.dig(:params, :textDocument, :uri)
    text = request.dig(:params, :contentChanges, 0, :text)
    store[uri] = text

    nil
  end

  on("textDocument/didOpen") do |request|
    uri = request.dig(:params, :textDocument, :uri)
    text = request.dig(:params, :textDocument, :text)
    store[uri] = text

    nil
  end

  on("textDocument/didClose") do |request|
    uri = request.dig(:params, :textDocument, :uri)
    store.delete(uri)

    nil
  end

  on("textDocument/foldingRange") do |request|
    respond_with_folding_ranges(request.dig(:params, :textDocument, :uri))
  end

  on("shutdown") { shutdown }
end

textDocument/didOpen, textDocument/didChange, textDocument/didClose では対象のファイルの uri - contents 対をハッシュとしてインメモリに保存・更新・削除する処理を行なっている。これは大多数の method のリクエストを行う際に uri のみの params で良くするのが狙い。

Add support for document opened, changed and closed events. We need those for every request. We basically keep a hash as storage where the key is the file URI and the value is the content of that file. Most other requests will only send us the URI and so we just access the hash to get the file contents

textDocument/foldingRange に関してはここで Ruby::Lsp::Requests::FoldingRanges クラスという Ruby プログラムのパース、FoldingRange[] の抽出などを行う実装が追加されている。なお textDocument/foldingRange とはその名の通りエディタ内でコード折り畳みをしてくれるあれである。この method にリクエストすることで対象 textDocument 内のどこが折り畳み可能か?それぞれどう折り畳むか?をエディタに知らせることができる。

これはコードを読むのが早いので以下に一部実装を抜粋している。

def initialize(source)
  @parser = SyntaxTree.new(source)
  @queue = [@parser.parse]
  @ranges = []
end

def run
  until @queue.empty?
    node = @queue.shift

    case node
    when SyntaxTree::Def
      location = node.location

      @ranges << LanguageServer::Protocol::Interface::FoldingRange.new(
    start_line: location.start_line - 1,
    end_line: location.end_line - 1,
    kind: "region"
      )
    else
      @queue.unshift(*node.child_nodes.compact)
    end
  end

  @ranges
end

ここではまず syntaxTree (CRubyで利用されている Ripper を wrap したライブラリ) を用いて受け取った Ruby プログラムの文字列をパースしている。その後 SyntaxTree::Def Node を走査して、見つかった各メソッドを折り畳み対象としている。

Store the parsed tree to avoid unnecessary re-parsing by vinistock · Pull Request #13 · Shopify/ruby-lsp · GitHub

10番のPRで追加された Store クラス (uri - contents 対ハッシュをインメモリに保持しているクラス) が contents の代わりに syntax_tree でパース済みの AST を保持するようになった。これで大部分の method でいちいち syntax_tree でパースする必要がなくなった。

Introduce a generic visitor pattern for requests handling by Morriar · Pull Request #19 · Shopify/ruby-lsp · GitHub

AST Node の走査に Visitor パターンが導入された。 このPRではまだ LSP の実装に使われていないが、 https://shopify.engineering/improving-the-developer-experience-with-ruby-lsp を読んでいるなかでも Visitor パターンの言及はあって、例えば以下のような書き方ができるようになっている。

class FoldingRange < SyntaxTree::Visitor
  def initialize(ast)
    @ast = ast
    @ranges = []
  end
  
  def run
    visit(@ast)
    @ranges
  end
  
  def visit_class(node)
    location = node.location
    
    @ranges << {
      startLine: location.start_line - 1,
      endLine: location.end_line - 1,
      kind: "region",
    }
  
    super
  end
  
  def visit_def(node)
    location = node.location
    
    @ranges << {
      startLine: location.start_line - 1,
      endLine: location.end_line - 1,
      kind: "region",
    }
  
    super
  end
end

Enhance code folding by vinistock · Pull Request #20 · Shopify/ruby-lsp · GitHub

FoldingRange が強化された。今まではシンプルに def のみ折り畳み可能だったが、ここでは class や if、elsif など計 17 ケースが新たにサポートされた。 ここからも少しづつサポート範囲が広がっていくが FoldingRange については個人的にあまり関心がないので積極的に拾わないことにする。

Implement DocumentSymbol requests handler by Morriar · Pull Request #21 · Shopify/ruby-lsp · GitHub

textDocument/documentSymbol の実装が追加された。 textDocument/documentSymbol とは与えられた textDocument の中にある symbol の種類や階層関係を取得するためのメソッド。

https://user-images.githubusercontent.com/583144/160193413-5d55a95e-45e2-4f89-97ad-6d72e28ab621.png

例えば symbol の種類は 3.17 時点で以下の種類が存在する。

// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol



/**
 * A symbol kind.
 */
export namespace SymbolKind {
    export const File = 1;
    export const Module = 2;
    export const Namespace = 3;
    export const Package = 4;
    export const Class = 5;
    export const Method = 6;
    export const Property = 7;
    export const Field = 8;
    export const Constructor = 9;
    export const Enum = 10;
    export const Interface = 11;
    export const Function = 12;
    export const Variable = 13;
    export const Constant = 14;
    export const String = 15;
    export const Number = 16;
    export const Boolean = 17;
    export const Array = 18;
    export const Object = 19;
    export const Key = 20;
    export const Null = 21;
    export const EnumMember = 22;
    export const Struct = 23;
    export const Event = 24;
    export const Operator = 25;
    export const TypeParameter = 26;
}

export type SymbolKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26;

基本的に syntax_tree がパースした AST Node のクラスと Kind が一致するので前述の Visitor パターンがとても刺さっていた。


長くなってしまったので今回はここまで。次回は https://github.com/Shopify/ruby-lsp/pull/32 から見ていく。

参考資料

Shopify/ruby-lsp の内部実装を読み解く (2) mtsmfm/language_server-protocol-ruby 編

概要

個人的に最近アツい https://github.com/Shopify/ruby-lsp の内部実装を読み解いて追加機能の提案・実装が出来るレベルを目指す。

なおこの回以外のブログは https://d.hatena.ne.jp/keyword/reading-shopify-ruby-lsp#related-blog で見ることができる。

前提

筆者に LSP (Language Server Protocol) の知識はほとんどない。クライアントとして利用しているのでどういったものかは知っているが、その内部実装には明るくない。なのでこの記事を読んでいる人で内容の誤りに気がついたらコメントして頂けるとありがたい。

また Ruby および TypeScript はそれなりに歴があるのでコードを読むことに不自由はない。

やること

https://github.com/Shopify/ruby-lsp のコミットを古い方から読んでいく。こういったコードリーディングはやったことないが、単に最新のソースコードを読むよりも Shopify の優秀なエンジニアの手癖を見ることが出来て学びが多そうなので実践してみる。

なお、あまり上手くいかなかったら違うアプローチに変えるつもり。

本編

前回に引き続き、今回は https://github.com/mtsmfm/language_server-protocol-ruby のコードリーディングを行う。

launguage_server-protocol-rubyRuby 向けの LSP SDK である。本筋はあくまで Shopify/ruby-lsp なので今回はサクッとコードを読んで実装・役割を把握しておく。

全体像

このライブラリで重要なモジュールは以下の3つ。

  • LanguageServer::Protocol::Transport モジュール
  • LanguageServer::Protocol::Interface モジュール
  • LanguageServer::Protocol::Constant モジュール

LanguageServer::Protocol::Transport モジュール

LanguageServer::Protocol::Transport::Io::Reader クラスおよび LanguageServer::Protocol::Transport::Io::Writer クラスの実装を持っているモジュール。

これらのクラスは非常にシンプルで Reader クラスは JSON-RPC リクエストの読み込みを、Writer クラスは JSON-RPC レスポンスの書き込みを、それぞれ任意のIOで行う責務をもつ。このことは以下のサンプルコードを見るとわかりやすい。

require "language_server-protocol"

LSP = LanguageServer::Protocol
writer = LSP::Transport::Stdio::Writer.new
reader = LSP::Transport::Stdio::Reader.new

subscribers = {
  initialize: -> {
    LSP::Interface::InitializeResult.new(
      capabilities: LSP::Interface::ServerCapabilities.new(
        text_document_sync: LSP::Interface::TextDocumentSyncOptions.new(
          change: LSP::Constant::TextDocumentSyncKind::FULL
        ),
        completion_provider: LSP::Interface::CompletionOptions.new(
          resolve_provider: true,
          trigger_characters: %w(.)
        ),
        definition_provider: true
      )
    )
  }
}

reader.read do |request|
  result = subscribers[request[:method].to_sym].call
  writer.write(id: request[:id], result: result)
  exit
end

この例では標準入出力を指定IOとしているため以下のようなコマンドでプログラムを実行することができる。

$ echo "Content-Length: 55\r\n\r\n{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"params\":{}}" | ruby ./test/example.rb

Content-Length: 185

{"id":null,"result":{"capabilities":{"textDocumentSync":{"change":1},"completionProvider":{"triggerCharacters":["."],"resolveProvider":true},"definitionProvider":true}},"jsonrpc":"2.0"}%

LanguageServer::Protocol::Interface モジュール

このモジュールには LSPの仕様において定義されている LSP メソッドのインターフェースやデータ構造(リクエストやレスポンスのフォーマットなど)が含まれている。

例えば LSP::Interface::ServerCapabilities というクラスは microsoft/language-server-protocol の _specifications/lsp/3.18/general/initialize.md の定義が元になっている。

module LanguageServer
  module Protocol
    module Interface
      class ServerCapabilities
        def initialize # 引数は省略
          @attributes = {}

          @attributes[:positionEncoding] = position_encoding if position_encoding
          # 省略

          @attributes.freeze
        end

        #
        # The position encoding the server picked from the encodings offered
        # by the client via the client capability `general.positionEncodings`.
        #
        # If the client didn't provide any position encodings the only valid
        # value that a server can return is 'utf-16'.
        #
        # If omitted it defaults to 'utf-16'.
        #
        # @return [string]
        def position_encoding
          attributes.fetch(:positionEncoding)
        end

        # 省略
    end
  end
end
interface ServerCapabilities {

    /**
    * The position encoding the server picked from the encodings offered
    * by the client via the client capability `general.positionEncodings`.
    *
    * If the client didn't provide any position encodings the only valid
    * value that a server can return is 'utf-16'.
    *
    * If omitted it defaults to 'utf-16'.
    *
    * @since 3.17.0
    */
    positionEncoding?: PositionEncodingKind;

    /**
    * Defines how text documents are synced. Is either a detailed structure
    * defining each notification or for backwards compatibility the
    * TextDocumentSyncKind number. If omitted it defaults to
    * `TextDocumentSyncKind.None`.
    */
    textDocumentSync?: TextDocumentSyncOptions | TextDocumentSyncKind;

    // 省略
}

なおこれらの Ruby プログラムは人力で書かれているわけではなく https://github.com/microsoft/language-server-protocol から自動生成されている。自動生成の仕組みについては後述する。

LanguageServer::Protocol::Constant モジュール

このモジュールには LSPの仕様において定義されている定数(エラーコード、イベントの種類、操作の種類など)が含まれている。

例えば LSP::Constant::TextDocumentSyncKind::FULL という定数は microsoft/language-server-protocol の _specifications/lsp/3.18/specification.md の定義が元になっている。

module LanguageServer
  module Protocol
    module Constant
      #
      # Defines how the host (editor) should sync document changes to the language
      # server.
      #
      module TextDocumentSyncKind
        #
        # Documents should not be synced at all.
        #
        NONE = 0
        #
        # Documents are synced by always sending the full content
        # of the document.
        #
        FULL = 1
        #
        # Documents are synced by sending the full content on open.
        # After that only incremental updates to the document are
        # sent.
        #
        INCREMENTAL = 2
      end
    end
  end
end
/**
 * Defines how the host (editor) should sync document changes to the language
 * server.
 */
export namespace TextDocumentSyncKind {
    /**
    * Documents should not be synced at all.
    */
    export const None = 0;

    /**
    * Documents are synced by always sending the full content
    * of the document.
    */
    export const Full = 1;

    /**
    * Documents are synced by sending the full content on open.
    * After that only incremental updates to the document are
    * sent.
    */
    export const Incremental = 2;
}

export type TextDocumentSyncKind = 0 | 1 | 2;

こちらも https://github.com/microsoft/language-server-protocol から自動生成されている(後述)。

自動生成

前述の LanguageServer::Protocol::Interface モジュールと LanguageServer::Protocol::Constant モジュールは LSP の公式仕様である https://github.com/microsoft/language-server-protocol から自動生成されており、そのロジックは https://github.com/mtsmfm/language_server-protocol-ruby/blob/v3.17.0.3/scripts/generateFiles.ts で見ることができる。

見るとわかるが、なぜか LSP の仕様は markdownmarkdown 内のコードブロック (TS) で管理されており、このスクリプトはそれらの markdown を気合いでパースして Ruby プログラムを出力する内容になっている。

一方で LSP 3.17 からは markdown ではなく json で仕様を管理する動きも見られる。mtsmfm/language_server-protocol-ruby もその流れに乗っかって脱 markdown パースを目指しているが 2023/12 時点では json 側の仕様が一部欠けているため実用には至っていない様子。 参考: https://github.com/mtsmfm/language_server-protocol-ruby/pull/49#issuecomment-1247458822

参考資料

Shopify/ruby-lsp の内部実装を読み解く (1) v0.0.1 の変更箇所、LSP の基本動作、LSIF (Language Server Index Format)

概要

個人的に最近アツい https://github.com/Shopify/ruby-lsp の内部実装を読み解いて追加機能の提案・実装が出来るレベルを目指す。

前提

筆者に LSP (Language Server Protocol) の知識はほとんどない。クライアントとして利用しているのでどういったものかは知っているが、その内部実装には明るくない。なのでこの記事を読んでいる人で内容の誤りに気がついたらコメントして頂けるとありがたい。

また Ruby および TypeScript はそれなりに歴があるのでコードを読むことに不自由はない。

やること

https://github.com/Shopify/ruby-lsp のコミットを古い方から読んでいく。こういったコードリーディングはやったことないが、単に最新のソースコードを読むよりも Shopify の優秀なエンジニアの手癖を見ることが出来て学びが多そうなので実践してみる。

なお、あまり上手くいかなかったら違うアプローチに変えるつもり。

本編

今日は初回なので v0.0.1 の commit を読む。

Initial commit · Shopify/ruby-lsp@90afd3e · GitHub

記念すべき initial commit。個人的に興味深かった点。

Add boilerplate LSP server · Shopify/ruby-lsp@873fa35 · GitHub

LSP らしい実装が初めて入った commit。ここで LSP のキャッチアップを済ませておく。

LSP とは

Official page for Language Server Protocol の記載が分かりやすい。

The Language Server Protocol (LSP) defines the protocol used between an editor or IDE and a language server that provides language features like auto complete, go to definition, find all references etc.

まず前提として、Language Server とは特定のプログラミング言語(あるはそれらに準ずる言語)において高度な言語機能、例えば補完やフォーマッタなどの機能を提供するサーバーのことである。例えば今回のテーマである Shopify/ruby-lsp は Language Server の1つである。

そして LSP (Language Server Protocol) とはこの Language Server と開発ツール間の通信プロトコルである。 このプロトコルのおかげで、世のエディタは高度な言語機能を特定言語に依存しない形で統一的に組み込むことができる。

LSP の動作原理

詳細はこの後の Shopify/ruby-lsp のコードリーディングから読み取るとして、ここでは https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/ の内容を基本として抑えておく。

まず LSP はサーバークライアント方式である。このことは次の図を見ると分かりやすい。Development Tool (クライアント) は JSON-RPC を用いて Language Server に Notification あるいは Request を送信する。そうすると Language Server は Development Tool に Notification あるいは Response を返す。

https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/

LSP は JSON-RPC プロトコルに則って定義した rpc を含んでおり、図中でも利用されている代表的な rpc として以下が挙げられる。

  • textDocument/didOpen
    • 新しい textDocument を開いた際にクライアントからサーバーに送信される Notification
  • textDocument/didChange
    • textDocument に変更を加えた際に送信される Notification
  • textDocument/publishDiagnostics
    • サーバーからクライアントへ push されるバリデーション結果の Notification
    • error や warning などの情報を含める
    • 結果のownershipはサーバーにあるのでサーバー側は必要に応じて diagnostics を clear したり merge したりする必要がある
  • textDocument/definition
    • 任意の位置の symbol の定義場所の解決を行いたい際にクライアントからサーバーへ送られる Request
  • textDocument/didClose
    • textDocument を閉じた際にクライアントからサーバーに送信される Notification

同時に2言語以上のファイルを編集する際には以下の図のように複数プロセスのLanguage Server と単一の Development Tools に対してそれぞれ上記の JSON-RPC を発行することになっている。

https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/

また LSP にはある Language Server・Development Tools が提供可能な言語機能のセットを表す Capabilities という概念がある。実装レベルでの話は置いておいて、ここでは前提知識として全ての Language Server や Development Tools が上記 JSON-RPC を網羅的にサポートしているわけではないことを頭に入れておくと良い。

LSIF (Language Server Index Format)

https://microsoft.github.io/language-server-protocol/overviews/lsif/overview/

LSP の関連技術として LSIF ("else if" と発音する) という技術が存在する。この技術は名前の通り LSP で度々計算されるファイル群に対する index を発行する技術で LSP 起動時に indexing files といった表示が出るのはこの LSIF が関連している(中身を見たわけではないので少し自信ない)

詳細な動作原理は後々追うとして、ここでは LSP と LSIF がそれぞれ異なる目的を持った技術であること、LSP によっては必要に応じて LSIF を活用していることを覚えておくと良い。

  • LSP の目的
    • リアルタイムなコード解析
  • LSIF の目的
    • 事前に用意したインデックスを用いたコードナビゲーション

LSIF の活用例: textDocument/hover の応答として毎回コード解析を行わずにインデックスを用いて高速に Response を返す。

ここまでで最低限必要な知識は得られたので、一旦 LSP のキャッチアップはここまでにして Shopify/ruby-lsp のコードリーディングに戻る。

(再開) Add boilerplate LSP server · Shopify/ruby-lsp@873fa35 · GitHub

この commit には以下のコードが含まれる。

# frozen_string_literal: true

require "language_server-protocol"

module Ruby
  module LSP
    module Cli
      def self.start(_argv)
        writer = LanguageServer::Protocol::Transport::Stdio::Writer.new
        reader = LanguageServer::Protocol::Transport::Stdio::Reader.new

        subscribers = {
          initialize: -> {
            LanguageServer::Protocol::Interface::InitializeResult.new(
              capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
                text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
                  change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL
                ),
                completion_provider: LanguageServer::Protocol::Interface::CompletionOptions.new(
                  resolve_provider: true,
                  trigger_characters: %w(.)
                ),
                definition_provider: true
              )
            )
          }
        }

        reader.read do |request|
          result = subscribers[request[:method].to_sym].call
          writer.write(id: request[:id], result: result)
          exit
        end
      end
    end
  end

上記コードと利用している third-party SDK のテストコードから分かることは以下の通り。

  • この LSP Server は initialize リクエストに対して capabilities を返すことができる
    • capabilities に含まれるメソッドは以下の通り
    • "capabilities"=>{"textDocumentSync"=>{"change"=>1}, "completionProvider"=>{"triggerCharacters"=>["."], "resolveProvider"=>true}, "definitionProvider"=>true}
    • textDocumentSync.change
    • ただし capabilities に含まれるメソッドは定義こそあれど実装は空なので実際には何もしない
  • reader.read do |request| ~ のプログラムで実際にクライアントから受け取った Request を処理している

さて、ここまで進んでみて、先に GitHub - mtsmfm/language_server-protocol-ruby: A Language Server Protocol SDK for Ruby にも一通り目を通しておくと理解が捗りそうなことに気がついた。この記事も長くなってしまったので今回は一旦終わりにしつつ次回は Shopify/ruby-lsp が内部で利用している mtsmfm/language_server-protocol-ruby のコードリーディングを行おうと思う。

参考資料

ハードスキル鍛えたい、と思いきや自分探し

プライベート用のメモ。基本的に自分用に書いたので人に見られたくない内容になっているので有料にした。こうすればみんな見れない。

この続きを読むには
購入して全文を読む