プラグインの拡張

このガイドでは、確立されたパターンに従ってプラグインに新機能を追加する方法を説明します。

一般的なワークフロー

  1. src/main/kotlin/com/rescript/plugin/ 配下の適切なパッケージに新しい Kotlin ファイルを作成

  2. IntelliJ Platform の Extension Point インターフェースを実装

  3. 正しい Extension Point の下に plugin.xml で登録

  4. src/test/kotlin/com/rescript/plugin/テストを追加

  5. ビルドとテスト: ./gradlew buildPlugin

一般的な Extension Point パターン

新しいインスペクションの追加

インスペクションはコードを分析し、問題を報告します。

package com.rescript.plugin.analysis

import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor

class RescriptMyInspection : LocalInspectionTool() {
    override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
        // Return a visitor that checks PSI elements
    }
}

plugin.xml で登録:

<localInspection
    language="ReScript"
    groupName="ReScript"
    displayName="My inspection"
    enabledByDefault="true"
    level="WARNING"
    implementationClass="com.rescript.plugin.analysis.RescriptMyInspection"/>

新しいインテンションアクションの追加

インテンションは Alt+Enter でクイックアクションを提供します。

package com.rescript.plugin.intention

import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile

class RescriptMyIntention : IntentionAction {
    override fun getText(): String = "My action"
    override fun getFamilyName(): String = "ReScript"
    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean { ... }
    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { ... }
    override fun startInWriteAction(): Boolean = true
}

plugin.xml で登録:

<intentionAction>
    <language>ReScript</language>
    <className>com.rescript.plugin.intention.RescriptMyIntention</className>
    <category>ReScript</category>
</intentionAction>

新しいアクションの追加

アクションはメニューに表示され、キーボードショートカットを割り当てることができます。

package com.rescript.plugin.navigation

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent

class RescriptMyAction : AnAction() {
    override fun actionPerformed(e: AnActionEvent) { ... }
    override fun update(e: AnActionEvent) { ... }
}

plugin.xml で登録:

<action id="Rescript.MyAction"
        class="com.rescript.plugin.navigation.RescriptMyAction"
        text="My Action"
        description="Description of my action">
    <add-to-group group-id="EditorPopupMenu" anchor="last"/>
    <keyboard-shortcut keymap="$default" first-keystroke="alt shift M"/>
</action>

新しいレクサートークンの追加

レクサーに新しいトークン型を追加するには:

  1. Rescript.flex を編集 — トークンルールを追加

  2. RescriptTokenTypes.kt を編集IElementType 定数を定義し、適切な TokenSet に追加

  3. RescriptSyntaxHighlighter.kt を編集 — トークンを TextAttributesKey にマッピング

  4. ビルド./gradlew buildPlugin でレクサーを再生成

Postfix テンプレートの追加

// Add to RescriptPostfixTemplateProvider.kt
class MyPostfixTemplate(provider: PostfixTemplateProvider) :
    PostfixTemplateWithExpressionSelector(
        "mytemplate",
        "expr.mytemplate",
        "Description",
        RescriptExpressionSelector(),
        provider
    ) {
    override fun expandForChooseExpression(expression: PsiElement, editor: Editor) {
        // Transform the expression
    }
}

Code Vision プロバイダーの追加

Code Vision プロバイダーは、コード要素の上にインラインアノテーション(例: 型シグネチャ)を表示します。

package com.rescript.plugin.codevision

import com.intellij.codeInsight.hints.codeVision.DaemonBoundCodeVisionProvider
import com.intellij.codeInsight.hints.codeVision.CodeVisionAnchorKind
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile

class RescriptMyCodeVisionProvider : DaemonBoundCodeVisionProvider {
    override val id: String = "rescript.myVision"
    override val name: String = "My Vision"
    override val defaultAnchor: CodeVisionAnchorKind = CodeVisionAnchorKind.Top

    override fun computeForEditor(
        editor: Editor,
        file: PsiFile,
    ): List<Pair<TextRange, CodeVisionEntry>> {
        // Compute entries to display above code elements
        return listOf(
            Pair(range, TextCodeVisionEntry("display text", id))
        )
    }
}

plugin.xml で登録:

<codeInsight.daemonBoundCodeVisionProvider
    implementation="com.rescript.plugin.codevision.RescriptMyCodeVisionProvider"/>

ツールウィンドウの追加

ツールウィンドウは、IDE のサイドバーまたはボトムバーに永続的な UI パネルを提供します。

package com.rescript.plugin.typeinfo

import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory

class RescriptMyToolWindowFactory : ToolWindowFactory, DumbAware {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        val panel = MyPanel(project)
        val content = toolWindow.contentManager.factory
            .createContent(panel, null, false)
        toolWindow.contentManager.addContent(content)
    }

    override fun shouldBeAvailable(project: Project): Boolean = true
}

plugin.xml で登録:

<toolWindow id="My Tool Window"
            anchor="bottom"
            secondary="true"
            factoryClass="com.rescript.plugin.typeinfo.RescriptMyToolWindowFactory"
            icon="/icons/rescript-file.svg"/>

PSI スタブインデックスの追加

スタブインデックスは、ファイル全体を解析せずに高速なシンボル検索を可能にします。プラグインは名前ベースのインデックスに StringStubIndexExtension を、コンテンツベースのインデックスに FileBasedIndexExtension を使用します。

名前インデックス(名前によるシンボル検索):

package com.rescript.plugin.indexing

import com.intellij.psi.stubs.StringStubIndexExtension
import com.intellij.psi.stubs.StubIndexKey

class RescriptMyIndex : StringStubIndexExtension<RescriptDeclarationPsiElement>() {
    companion object {
        val KEY: StubIndexKey<String, RescriptDeclarationPsiElement> =
            StubIndexKey.createIndexKey("rescript.my.index")
    }

    override fun getKey() = KEY
    override fun getVersion(): Int = 1
}

plugin.xml で登録:

<stubIndex implementation="com.rescript.plugin.indexing.RescriptMyIndex"/>

ファイルベースインデックス(クエリ用のファイルコンテンツインデックス):

class RescriptMyFileIndex : FileBasedIndexExtension<String, Void>() {
    companion object {
        val NAME: ID<String, Void> = ID.create("rescript.my.file.index")
    }

    override fun getName() = NAME
    override fun getIndexer(): DataIndexer<String, Void, FileContent> {
        return DataIndexer { inputData ->
            // Scan file content and return key-value map
            mapOf("key" to null)
        }
    }
    override fun getKeyDescriptor() = EnumeratorStringDescriptor()
    override fun getValueExternalizer() = VoidDataExternalizer()
    override fun getVersion(): Int = 1
    override fun getInputFilter() = FileBasedIndex.InputFilter { file ->
        file.fileType == RescriptFileType.INSTANCE
    }
    override fun dependsOnFileContent(): Boolean = true
}

plugin.xml で登録:

<fileBasedIndex implementation="com.rescript.plugin.indexing.RescriptMyFileIndex"/>

LSP カスタムリクエストの追加

@JsonRequest アノテーションを使用して、LSP サーバーインターフェースにカスタムリクエストメソッドを追加します。

package com.rescript.plugin.lsp

import org.eclipse.lsp4j.TextDocumentIdentifier
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
import org.eclipse.lsp4j.services.LanguageServer
import java.util.concurrent.CompletableFuture

interface RescriptLanguageServer : LanguageServer {
    @JsonRequest("textDocument/createInterface")
    fun createInterface(params: TextDocumentIdentifier): CompletableFuture<TextDocumentIdentifier>

    @JsonRequest("textDocument/openCompiled")
    fun openCompiled(params: TextDocumentIdentifier): CompletableFuture<TextDocumentIdentifier>
}

カスタムリクエストはインターフェースメソッドとして定義され、plugin.xml には登録しません。このインターフェースは LspServerDescriptor 経由で LSP サーバープロキシを取得する際に使用されます:

val server = lspServerDescriptor.getServer() as? RescriptLanguageServer
val result = server?.createInterface(params)?.get()

ペーストプロセッサの追加

ペーストプロセッサは、エディタへのペースト時にクリップボードの内容を変換します。

package com.rescript.plugin.paste

import com.intellij.codeInsight.editorActions.CopyPastePostProcessor
import com.intellij.codeInsight.editorActions.TextBlockTransferableData
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import java.awt.datatransfer.Transferable

class RescriptMyPasteProcessor : CopyPastePostProcessor<TextBlockTransferableData>() {
    override fun collectTransferableData(
        file: PsiFile, editor: Editor,
        startOffsets: IntArray, endOffsets: IntArray,
    ): List<TextBlockTransferableData> = emptyList()

    override fun extractTransferableData(
        content: Transferable,
    ): List<TextBlockTransferableData> {
        // Check clipboard content and return data if applicable
    }

    override fun processTransferableData(
        project: Project, editor: Editor,
        bounds: RangeMarker, caretOffset: Int,
        indented: Ref<in Boolean>,
        values: MutableList<out TextBlockTransferableData>,
    ) {
        // Transform the pasted content
        WriteCommandAction.runWriteCommandAction(project) {
            editor.document.replaceString(bounds.startOffset, bounds.endOffset, transformed)
        }
    }
}

plugin.xml で登録:

<copyPastePostProcessor
    implementation="com.rescript.plugin.paste.RescriptMyPasteProcessor"/>

ファイル命名規則

  • クラス: Rescript<Feature><Type>.kt (例: RescriptFoldingBuilder.kt)

  • テスト: Rescript<Feature><Type>Test.kt (例: RescriptFoldingBuilderTest.kt)

  • パッケージ: 機能カテゴリに合わせる (例: folding/, navigation/, analysis/)

KDoc 要件

すべてのクラスと非自明な公開メソッドには英語で KDoc コメントを記述する必要があります。詳細は Contributing Guide を参照してください。