アーキテクチャ¶
ReScript IntelliJ Plugin は、組み込みレクサーと外部 Language Server を組み合わせたハイブリッドアーキテクチャを採用しています。
概要¶
graph TD
subgraph IDE["JetBrains IDE"]
subgraph Plugin["ReScript IntelliJ Plugin"]
subgraph L1["Layer 1: Language Foundation"]
Lexer["JFlex Lexer"]
Parser["Lightweight Parser"]
PSI["PSI Tree"]
Lexer --> Parser --> PSI
PSI --> Folding["Code Folding"]
PSI --> Structure["Structure View"]
PSI --> Highlight["Syntax Highlighting"]
PSI --> Mover["Statement Mover"]
end
subgraph L2["Layer 2: LSP Integration"]
Completion["Completion"]
Diagnostics["Diagnostics"]
Navigation["Navigation"]
Hover["Hover"]
SemanticTokens["Semantic Tokens"]
CodeLens["Code Lens"]
InlayHints["Inlay Hints"]
end
subgraph L3["Layer 3: IDE Integration"]
RunConfig["Run Configurations"]
Formatter["External Formatter"]
Inspections["Code Inspections"]
TestRunner["Test Runner"]
Reanalyze["Dead Code Analysis"]
end
end
end
L2 -->|stdio| LSP["@rescript/language-server<br/>(Node.js process)"]
PSI ツリー処理パイプライン¶
flowchart LR
Source[".res Source File"] --> Lexer["JFlex Lexer<br/>(Rescript.flex)"]
Lexer --> Tokens["Token Stream<br/>(RescriptTokenTypes)"]
Tokens --> Parser["Lightweight Parser<br/>(RescriptParser)"]
Parser --> PSI["PSI Tree"]
PSI --> Folding["Code Folding"]
PSI --> StructureView["Structure View"]
PSI --> Mover["Statement Mover"]
PSI --> Breadcrumb["Breadcrumbs"]
PSI --> StubIndex["Stub Index"]
PSI --> CallHierarchy["Call Hierarchy"]
LSP リクエストフロー¶
sequenceDiagram
participant User
participant IDE as JetBrains IDE
participant Plugin as ReScript Plugin
participant LSP as Language Server
User->>IDE: Open .res file
IDE->>Plugin: File opened event
Plugin->>Plugin: Check extension (.res/.resi)
Plugin->>LSP: ensureServerStarted()
Note over Plugin,LSP: stdio connection
User->>IDE: Type code
IDE->>Plugin: Document changed
Plugin->>LSP: textDocument/didChange
LSP->>Plugin: textDocument/publishDiagnostics
Plugin->>IDE: Show errors/warnings
User->>IDE: Request completion
Plugin->>LSP: textDocument/completion
LSP->>Plugin: CompletionList
Plugin->>IDE: Show completions
User->>IDE: Ctrl+Click symbol
Plugin->>LSP: textDocument/definition
LSP->>Plugin: Location
Plugin->>IDE: Navigate to definition
LSP->>Plugin: rescript/compilationStatus
Plugin->>IDE: Update status bar
レイヤー 1: 言語基盤¶
このレイヤーはプラグインに完全に組み込まれており、外部依存なしで動作します。
JFlex レクサー¶
ソース:
src/main/java/com/rescript/plugin/lang/Rescript.flex生成ファイル:
RescriptFlexLexer.java(自動生成、コミット対象外)ラッパー:
RescriptLexer.kt(FlexAdapter)
レクサーは ReScript ソースコードを RescriptTokenTypes.kt で定義されたトークン型にトークン化します。以下を処理します:
キーワード、識別子、リテラル、演算子、句読点
ネストされたブロックコメント (
/* /* */ */)テンプレート文字列補間 (
`${expr}`)宣言コンテキスト追跡 (
let/type→ 識別子分類)
軽量パーサー¶
ソース:
RescriptParser.kt
パーサーはトップレベル宣言のみを認識し、式、型、JSX の詳細な解析は行いません。これは意図的な設計であり、複雑な解析は Language Server に委譲しています。
認識される宣言:
let/let recバインディングtype/type rec定義module/module type/module rec宣言externalバインディング (FFI)open/includeディレクティブexception宣言@decoratorアノテーション
PSI ツリー¶
パーサーは以下の機能で使用される PSI (Program Structure Interface) ツリーを生成します:
コード折りたたみ — 複数行の宣言を折りたたみ
ストラクチャービュー — ファイルのアウトラインを表示
ステートメントムーバー — 宣言の上下移動
レイヤー 2: LSP 統合¶
このレイヤーは stdio 経由で ReScript Language Server と通信します。
主要クラス¶
クラス |
役割 |
|---|---|
|
LSP サーバーの起動タイミングを判定 |
|
サーバーの検出と起動を設定 |
|
カスタム LSP リクエストインターフェース |
|
カスタム LSP 通知受信クライアント |
|
LSP セマンティックトークンを色にマッピング |
|
LSP 通知からビルド状態を追跡 |
各クラスの責務の詳細¶
RescriptLspServerSupportProvider¶
ファイル: src/main/kotlin/com/rescript/plugin/lsp/RescriptLspServerSupportProvider.kt
このクラスは IntelliJ Platform LSP API の LspServerSupportProvider インターフェースを実装しています。エディタでファイルが開かれるたびにプラットフォームから呼び出されるエントリーポイントです。その唯一の責務は、指定されたファイルに対して LSP サーバーを起動すべきかどうかを判定することです。
判定ロジックはシンプルです。開かれたファイルの拡張子が .res または .resi であれば、プロバイダーは新しい RescriptLspServerDescriptor インスタンスを使用して serverStarter.ensureServerStarted() を呼び出します。ensureServerStarted メソッドは冪等です --- サーバーがプロジェクトで既に実行中の場合は何もしません。ファイル拡張子が一致しない場合、プロバイダーは何もせずに返します。
認識される拡張子のリストは companion object に次のように定義されています: 認識される拡張子のリストはコンパニオンオブジェクトで RESCRIPT_EXTENSIONS = setOf("res", "resi") として定義されており、一貫性のために RescriptLspServerDescriptor.isSupportedFile() でも再利用されます。
このクラスにはサーバーの設定、検出、ライフサイクル管理のロジックは含まれていません。プラットフォームのファイルオープンイベントと LSP サーバーのライフサイクルを橋渡しするトリガーとして機能するだけです。
RescriptLspServerDescriptor¶
ファイル: src/main/kotlin/com/rescript/plugin/lsp/RescriptLspServerDescriptor.kt
このクラスは ProjectWideLspServerDescriptor を継承し、Language Server プロセスの検出、設定、起動のすべてを担当します。
サーバー検出順序:
createCommandLine() メソッドは以下の手順で Language Server バイナリを解決します:
カスタムパスの確認: ユーザーが Settings > Languages & Frameworks > ReScript でカスタム LSP サーバーパスを設定している場合、そのパスが直接使用されます。これにより、開発者は Language Server の開発ビルドや特定のバージョンを指定できます。
プロジェクトローカルの
node_modules: プラグインは<project-root>/node_modules/.bin/rescript-language-serverに実行可能なバイナリがあるかを確認します。@rescript/language-serverがプロジェクトの依存関係としてインストールされている標準的なケースに対応します。プロジェクト内の JS フォールバック:
.binの実行ファイルが見つからない場合、<project-root>/node_modules/@rescript/language-server/out/cli.jsを確認します。.binのシンボリックリンクが作成されなかったケース(特定のインストール方法や Windows 環境など)に対応します。親ディレクトリの走査 (monorepo): プラグインはプロジェクトルートから各親ディレクトリに向かって走査し、各レベルで
node_modules/.bin/とnode_modules/@rescript/language-server/out/cli.jsを確認します。@rescript/language-serverがワークスペースルートにホイストされている monorepo 構成をサポートします。グローバル PATH 検索: 最後のフォールバックとして、プラグインは
which rescript-language-server(Unix) またはwhere rescript-language-server(Windows) を実行して、グローバルにインストールされたバイナリを検索します。エラー: 上記のいずれの手順も成功しない場合、Language Server のインストール方法を説明するユーザー向けメッセージとともに
ExecutionExceptionがスローされます。
起動設定:
解決されたパスが .js で終わる場合、サーバーは node <path> --stdio として起動されます(設定のカスタム Node.js パスまたは PATH の node を使用)。それ以外の場合、バイナリは --stdio で直接実行されます。作業ディレクトリはプロジェクトルートに設定されます。
初期化オプション:
createInitializationOptions() メソッドは初期化時に Language Server に設定を送信します。現在、以下を送信します:
Code Lens サポートを有効にするための
extensionConfiguration.codeLens: true。ユーザーの設定に基づいてインクリメンタル型チェックを制御するための
extensionConfiguration.incrementalTypechecking.enabled。
カスタマイズフック:
ディスクリプタは 2 つの重要なカスタマイズを設定します:
カスタムリクエストメソッドを有効にするため、
lsp4jServerClassにRescriptLanguageServer::class.javaを設定。カスタム通知を処理するため、
createLsp4jClient()がRescriptLsp4jClientインスタンスを返す。セマンティックハイライトのため、
lspCustomization.semanticTokensCustomizerにRescriptSemanticTokensSupportインスタンスを設定。
RescriptLanguageServer¶
ファイル: src/main/kotlin/com/rescript/plugin/lsp/RescriptLanguageServer.kt
これはクラスではなくインターフェースであり、LSP4J の標準 LanguageServer インターフェースを継承しています。ReScript Language Server 固有の JSON-RPC リクエストメソッドを宣言しており、標準 LSP プロトコルには含まれないものです。
カスタムリクエスト:
textDocument/createInterface---.resファイルを指すTextDocumentIdentifierを受け取り、Language Server にそこから.resiインターフェースファイルを生成するよう要求します。サーバーは生成されたファイルを指すTextDocumentIdentifierを返します。これはRescriptCreateInterfaceActionが「Create Interface」ナビゲーションアクションを提供するために使用されます。textDocument/openCompiled---.resファイルを指すTextDocumentIdentifierを受け取り、Language Server にコンパイル済み JavaScript 出力へのパスを解決するよう要求します。サーバーは.jsファイルを指すTextDocumentIdentifierを返します。これはRescriptOpenCompiledJsActionと Compiled JS Preview パネルで使用されます。
両メソッドは @JsonRequest アノテーションが付与されており、LSP4J が JSON-RPC のシリアライズとディスパッチを自動的に処理します。非同期実行のために CompletableFuture を返します。
LanguageServer を直接使用せずカスタムインターフェースを定義する理由は、LSP4J が lsp4jServerClass で指定されたインターフェース型に対してリフレクションを使用して利用可能なメソッドを検出するためです。サブインターフェースでこれらのメソッドを宣言することで、プラグインは標準の LSP4J リクエスト機構を通じてそれらを呼び出し可能にしています。
RescriptLsp4jClient¶
ファイル: src/main/kotlin/com/rescript/plugin/lsp/RescriptLsp4jClient.kt
このクラスは Lsp4jClient を継承し、Language Server から IDE に送信される ReScript 固有の通知を処理します。標準 LSP クライアントが標準通知(診断、ログメッセージなど)を処理する一方、このサブクラスはカスタム通知のサポートを追加します。
処理する通知:
rescript/compilationStatus--- この通知は、ビルド状態が変化するたびに ReScript Language Server から送信されます(例: コンパイル開始、コンパイル成功、エラーによるコンパイル失敗)。通知には以下のパラメータが含まれます:project--- プロジェクト名。projectRootPath--- ReScript プロジェクトのルートパス。status--- コンパイル状態を示す文字列 (例:status--- コンパイル状態を示す文字列(例:"success","failed","building")。errorCount--- コンパイルエラーの数。warningCount--- コンパイル警告の数。
この通知を受信すると、クライアントはプロジェクトが破棄されていないことを確認し(シャットダウン時のエラーを防ぐため)、RescriptCompilationStatusService.updateStatus() に委譲して新しい状態を保存し、登録済みのリスナー(ステータスバーウィジェットなど)に通知します。
compilationStatus メソッドの @JsonNotification アノテーションは、受信した rescript/compilationStatus メッセージをリフレクション経由でこのメソッドにルーティングするよう LSP4J に指示します。@Suppress("unused") アノテーションは、このメソッドが Kotlin コードから直接呼び出されることはなく、LSP4J のリフレクションベースのディスパッチによってのみ呼び出されるために付与されています。
RescriptSemanticTokensSupport¶
ファイル: src/main/kotlin/com/rescript/plugin/lsp/RescriptSemanticTokensSupport.kt
このクラスは LspSemanticTokensSupport を継承し、LSP セマンティックトークンこのクラスは LspSemanticTokensSupport を継承し、LSP セマンティックトークン型("variable", "type", "namespace" などの文字列)を IntelliJ の TextAttributesKey インスタンスにマッピングしてエディタでのレンダリングに使用します。
トークン型マッピング:
LSP トークン型 |
IntelliJ TextAttributesKey |
表示上の用途 |
|---|---|---|
|
|
ローカル変数、関数パラメータ |
|
|
型名(例: |
|
|
モジュール名(セマンティック解決済み) |
|
|
バリアントコンストラクタ(例: |
|
|
レコードフィールド名 |
|
|
JSX HTML 要素名(例: |
|
|
演算子(セマンティック解決済み) |
|
|
JSX タグブラケット(セマンティック解決済み) |
フォールバック動作: Language Server が上記にないトークン型を返した場合(例: プロトコルへの将来の追加)、getTextAttributesKey() メソッドは null を返します。これにより IntelliJ Platform はそのトークンに対してレクサーベースのハイライトを保持し、認識されないセマンティックトークン型によってハイライトが消えることを防ぎます。
常時有効動作: shouldAskServerForSemanticTokens() メソッドは無条件に true を返します。つまり、LSP が接続されている場合、プラグインは常にセマンティックトークンを要求します。これにより、Language Server が実行されるとすぐに、ユーザーの操作なしでセマンティックハイライトが有効になります。
getTextAttributesKey() の modifiers パラメータは現在 ReScript Language Server では使用されていませんが、将来の使用に備えて利用可能です(例: readonly プロパティや deprecated 識別子の区別)。
RescriptCompilationStatusService¶
ファイル: src/main/kotlin/com/rescript/plugin/lsp/RescriptCompilationStatusService.kt
これはプロジェクトレベルのサービス (@Service(Service.Level.PROJECT)) であり、ReScript プロジェクトの現在のコンパイル状態を保持します。ビルド状態情報の集中ストアおよびイベントバスとして機能します。
状態管理:
サービスは CompilationStatus 型の @Volatile プロパティ currentStatus を 1 つ保持しています。これは 3 つのフィールドを含むデータクラスです:
status: String--- ビルド状態。通常"success","failed","building", または"unknown"。errorCount: Int--- 最後のビルドにおけるコンパイルエラーの数。warningCount: Int--- 最後のビルドにおけるコンパイル警告の数。
初期状態は CompilationStatus.UNKNOWN(status "unknown"、エラー 0、警告 0)です。
更新フロー:
ReScript Language Server がビルド状態の変化時に
rescript/compilationStatus通知を送信します。RescriptLsp4jClient.compilationStatus()が通知を受信し、このサービスのupdateStatus()を呼び出します。updateStatus()がcurrentStatusに新しい状態を保存し、登録済みの全リスナーを走査して各リスナーのstatusChanged()を呼び出します。
リスナー管理:
ビルド状態の変化に反応する必要があるコンポーネント(ステータスバーウィジェット RescriptCompilerStatusWidgetFactory など)は、addListener(disposable, listener) を通じて CompilationStatusListener を登録します。リスナーは提供された Disposable が破棄されると自動的に削除され、ツールウィンドウやウィジェットが閉じられた際のメモリリークを防ぎます。
CompilationStatusListener は関数型インターフェース (fun interface) として定義されているため、リスナーをラムダで登録できます:
service.addListener(disposable) { status ->
// Update UI based on status.status, status.errorCount, etc.
}
スレッドセーフティ: currentStatus プロパティは @Volatile が付与されており、スレッド間の可視性を保証します。updateStatus() は LSP メッセージ処理スレッドから呼び出される一方、currentStatus は EDT (UI スレッド) から読み取られる可能性があるためです。
LSP サーバー検出¶
プラグインは以下の順序で Language Server を検索します:
Settings > Languages & Frameworks > ReScript で設定されたカスタムパス
node_modules/.bin/rescript-language-server(プロジェクトローカルバイナリ)node_modules/@rescript/language-server/out/cli.js(JS フォールバック)親ディレクトリの
node_modules/.bin/(monorepo サポート、ルートまで走査)which/whereによるグローバルインストール
カスタム LSP 拡張¶
プラグインは標準プロトコルに加えて ReScript 固有の LSP リクエストを使用します:
textDocument/createInterface—.resから.resiを生成textDocument/openCompiled— コンパイル済み JS パスを取得rescript/compilationStatus— ビルド状態の通知
レイヤー 3: IDE 統合¶
両レイヤーの上に構築される追加の IDE 機能:
実行構成 — ReScript ビルドコマンドの実行
外部フォーマッタ —
rescript formatCLI 連携コードインスペクション — ローカル分析(重複 open、空モジュール)
reanalyze 連携 — 外部ツールによるデッドコード分析
テストランナー — SMTestRunner による Jest/Vitest 連携
設計原則¶
複雑な解析を LSP に委譲 — プラグインは ReScript を完全に解析しようとしません。式レベルの理解は Language Server から提供されます。
グレースフルデグラデーション — Language Server が利用できない場合でも、すべてのネイティブ機能(ハイライト、折りたたみ、ストラクチャービュー)は動作します。
プラットフォーム API の使用 — プラグインは LSP をゼロから実装するのではなく、IntelliJ Platform の公式 LSP API (
com.intellij.platform.lsp) を使用します。コードベースをコンパクトに — Language Server を活用することで、プラグインは複雑な ReScript 型システムと分析ロジックの重複を回避します。