アーキテクチャ

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 と通信します。

主要クラス

クラス

役割

RescriptLspServerSupportProvider

LSP サーバーの起動タイミングを判定

RescriptLspServerDescriptor

サーバーの検出と起動を設定

RescriptLanguageServer

カスタム LSP リクエストインターフェース

RescriptLsp4jClient

カスタム LSP 通知受信クライアント

RescriptSemanticTokensSupport

LSP セマンティックトークンを色にマッピング

RescriptCompilationStatusService

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 バイナリを解決します:

  1. カスタムパスの確認: ユーザーが Settings > Languages & Frameworks > ReScript でカスタム LSP サーバーパスを設定している場合、そのパスが直接使用されます。これにより、開発者は Language Server の開発ビルドや特定のバージョンを指定できます。

  2. プロジェクトローカルの node_modules: プラグインは <project-root>/node_modules/.bin/rescript-language-server に実行可能なバイナリがあるかを確認します。@rescript/language-server がプロジェクトの依存関係としてインストールされている標準的なケースに対応します。

  3. プロジェクト内の JS フォールバック: .bin の実行ファイルが見つからない場合、<project-root>/node_modules/@rescript/language-server/out/cli.js を確認します。.bin のシンボリックリンクが作成されなかったケース(特定のインストール方法や Windows 環境など)に対応します。

  4. 親ディレクトリの走査 (monorepo): プラグインはプロジェクトルートから各親ディレクトリに向かって走査し、各レベルで node_modules/.bin/node_modules/@rescript/language-server/out/cli.js を確認します。@rescript/language-server がワークスペースルートにホイストされている monorepo 構成をサポートします。

  5. グローバル PATH 検索: 最後のフォールバックとして、プラグインは which rescript-language-server (Unix) または where rescript-language-server (Windows) を実行して、グローバルにインストールされたバイナリを検索します。

  6. エラー: 上記のいずれの手順も成功しない場合、Language Server のインストール方法を説明するユーザー向けメッセージとともに ExecutionException がスローされます。

起動設定:

解決されたパスが .js で終わる場合、サーバーは node <path> --stdio として起動されます(設定のカスタム Node.js パスまたは PATH の node を使用)。それ以外の場合、バイナリは --stdio で直接実行されます。作業ディレクトリはプロジェクトルートに設定されます。

初期化オプション:

createInitializationOptions() メソッドは初期化時に Language Server に設定を送信します。現在、以下を送信します:

  • Code Lens サポートを有効にするための extensionConfiguration.codeLens: true

  • ユーザーの設定に基づいてインクリメンタル型チェックを制御するための extensionConfiguration.incrementalTypechecking.enabled

カスタマイズフック:

ディスクリプタは 2 つの重要なカスタマイズを設定します:

  • カスタムリクエストメソッドを有効にするため、lsp4jServerClassRescriptLanguageServer::class.java を設定。

  • カスタム通知を処理するため、createLsp4jClient()RescriptLsp4jClient インスタンスを返す。

  • セマンティックハイライトのため、lspCustomization.semanticTokensCustomizerRescriptSemanticTokensSupport インスタンスを設定。

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

表示上の用途

variable

SEMANTIC_VARIABLE

ローカル変数、関数パラメータ

type

SEMANTIC_TYPE

型名(例: int, option, ユーザー定義型)

namespace

SEMANTIC_NAMESPACE

モジュール名(セマンティック解決済み)

enumMember

SEMANTIC_ENUM_MEMBER

バリアントコンストラクタ(例: Some, None, Ok

property

SEMANTIC_PROPERTY

レコードフィールド名

interface

SEMANTIC_INTERFACE

JSX HTML 要素名(例: div, span

operator

SEMANTIC_OPERATOR

演算子(セマンティック解決済み)

modifier

SEMANTIC_MODIFIER

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)です。

更新フロー:

  1. ReScript Language Server がビルド状態の変化時に rescript/compilationStatus 通知を送信します。

  2. RescriptLsp4jClient.compilationStatus() が通知を受信し、このサービスの updateStatus() を呼び出します。

  3. 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 を検索します:

  1. Settings > Languages & Frameworks > ReScript で設定されたカスタムパス

  2. node_modules/.bin/rescript-language-server (プロジェクトローカルバイナリ)

  3. node_modules/@rescript/language-server/out/cli.js (JS フォールバック)

  4. 親ディレクトリの node_modules/.bin/ (monorepo サポート、ルートまで走査)

  5. which / where によるグローバルインストール

カスタム LSP 拡張

プラグインは標準プロトコルに加えて ReScript 固有の LSP リクエストを使用します:

  • textDocument/createInterface.res から .resi を生成

  • textDocument/openCompiled — コンパイル済み JS パスを取得

  • rescript/compilationStatus — ビルド状態の通知

レイヤー 3: IDE 統合

両レイヤーの上に構築される追加の IDE 機能:

  • 実行構成 — ReScript ビルドコマンドの実行

  • 外部フォーマッタrescript format CLI 連携

  • コードインスペクション — ローカル分析(重複 open、空モジュール)

  • reanalyze 連携 — 外部ツールによるデッドコード分析

  • テストランナー — SMTestRunner による Jest/Vitest 連携

設計原則

  1. 複雑な解析を LSP に委譲 — プラグインは ReScript を完全に解析しようとしません。式レベルの理解は Language Server から提供されます。

  2. グレースフルデグラデーション — Language Server が利用できない場合でも、すべてのネイティブ機能(ハイライト、折りたたみ、ストラクチャービュー)は動作します。

  3. プラットフォーム API の使用 — プラグインは LSP をゼロから実装するのではなく、IntelliJ Platform の公式 LSP API (com.intellij.platform.lsp) を使用します。

  4. コードベースをコンパクトに — Language Server を活用することで、プラグインは複雑な ReScript 型システムと分析ロジックの重複を回避します。