diff --git a/CHANGELOG.md b/CHANGELOG.md index aac047f..7e6321f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,15 @@ All Moodle Dev plugin changes will be documented here The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0). -## [Unreleased] +## [Unreleased] + +### Added +- Best-effort integration with JetBrains AI Assistant (com.intellij.ml.llm): when Moodle framework is enabled, the plugin tries to update AI prompts. + - "Write Documentation > PHP" prompt now instructs to follow Moodle PHPDoc rules and to add @covers only in unit tests. + - "Built-In Actions > Commit Message generation" prompt now follows Moodle git commit policy and format. + +### Changed +- Bump plugin version to 2.2.1. ### Added diff --git a/src/main/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiLlmConfigurator.kt b/src/main/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiLlmConfigurator.kt new file mode 100644 index 0000000..be11c9d --- /dev/null +++ b/src/main/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiLlmConfigurator.kt @@ -0,0 +1,65 @@ +package il.co.sysbind.intellij.moodledev.ai + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import il.co.sysbind.intellij.moodledev.project.MoodleProjectSettings +import il.co.sysbind.intellij.moodledev.util.PluginUtil + +open class MoodleAiLlmConfigurator : ProjectActivity { + private val log = Logger.getInstance(MoodleAiLlmConfigurator::class.java) + + override suspend fun execute(project: Project) { + val settingsService = project.getService(MoodleProjectSettings::class.java) ?: return + val enabled = settingsService.settings.pluginEnabled + if (!enabled) return + + if (!isAiPluginInstalled()) { + log.debug("LLM plugin not installed; skipping Moodle AI prompt configuration") + return + } + + applyAIPrompts() + } + + // Visible for testing + protected open fun isAiPluginInstalled(): Boolean = PluginUtil.isPluginInstalled("com.intellij.ml.llm") + + // Visible for testing + protected open fun applyAIPrompts() = tryApplyAIPrompts() + + private fun tryApplyAIPrompts() { + val phpDoc = MoodleAiPrompts.phpDocPrompt + val commit = MoodleAiPrompts.commitMessagePrompt + + var applied = false + + // Attempt 1: Hypothetical PromptRepository in com.intellij.ml.llm + applied = applied or runCatching { + val repoClass = Class.forName("com.intellij.ml.llm.settings.prompts.PromptRepository") + val getInstance = repoClass.getMethod("getInstance") + val instance = getInstance.invoke(null) + val setTemplate = repoClass.getMethod("setTemplate", String::class.java, String::class.java, String::class.java) + setTemplate.invoke(instance, "Write Documentation", "PHP", phpDoc) + setTemplate.invoke(instance, "Commit Message", "", commit) + true + }.getOrElse { false } + + // Attempt 2: Hypothetical PromptTemplateRegistry in AI Actions + applied = applied or runCatching { + val registryClass = Class.forName("com.intellij.aiactions.prompt.PromptTemplateRegistry") + val getInstance = registryClass.getMethod("getInstance") + val instance = getInstance.invoke(null) + val setById = registryClass.getMethod("setTemplate", String::class.java, String::class.java) + setById.invoke(instance, "write.documentation.php", phpDoc) + setById.invoke(instance, "commit.message.generate", commit) + true + }.getOrElse { false } + + if (!applied) { + log.info("Moodle AI prompts could not be applied (no known registry found). This is safe to ignore if the AI plugin does not expose public APIs for templates.") + } else { + log.info("Moodle AI prompts applied successfully (best-effort).") + } + } +} diff --git a/src/main/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiPrompts.kt b/src/main/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiPrompts.kt new file mode 100644 index 0000000..83bd4a8 --- /dev/null +++ b/src/main/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiPrompts.kt @@ -0,0 +1,49 @@ +package il.co.sysbind.intellij.moodledev.ai + +object MoodleAiPrompts { + // Updated "Write Documentation > PHP" content per Moodle coding style + val phpDocPrompt: String = buildString { + appendLine("According to https://moodledev.io/general/development/policies/codingstyle#documentation-and-comments") + appendLine("Write PHPDoc for the given code.") + appendLine() + appendLine("Rules:") + appendLine("- Use Moodle PHPDoc style and tags.") + appendLine("- In unit tests only, add @covers for the relevant class and functions actually tested.") + appendLine("- Do not add @covers in non‑test code.") + } + + // Updated Built-In Actions > Commit Message generation template + val commitMessagePrompt: String = buildString { + appendLine("Make commit according to https://moodledev.io/general/development/policies/codingstyle#git-commits") + appendLine("Format your commit messages following this structure:") + appendLine() + appendLine("<$GIT_BRANCH_NAME> : ") + appendLine() + appendLine("") + appendLine() + appendLine("Guidelines:") + appendLine("- First line should be no more than 72 characters") + appendLine("- Use imperative form for summary (e.g., \"Add\" not \"Added\")") + appendLine("- Leave blank line after summary") + appendLine("- Keep detailed explanation to 2-3 sentences") + appendLine("- Include motivation and contrast with previous behavior") + appendLine() + appendLine("Example:") + appendLine("issue7685 mod_quiz: Add time extension support for quiz attempts") + appendLine() + appendLine("Implements ability to grant individual students extra time for quiz attempts.") + appendLine("This change allows teachers to accommodate students needing special arrangements") + appendLine("while maintaining the standard time limits for others.") + appendLine() + appendLine("Important Notes:") + appendLine("- For submodule commits, use the submodule's branch name") + appendLine("- Code area should be human-readable (e.g., 'gradebook' vs 'local_hujigradebook')") + appendLine("- Avoid including multiple unrelated changes") + appendLine("- Don't document the development process, only the final result") + appendLine() + appendLine("General Practices:") + appendLine("- Keep PRs focused and manageable") + appendLine("- Update documentation with changes") + appendLine("- Follow branching strategy") + } +} diff --git a/src/main/kotlin/il/co/sysbind/intellij/moodledev/util/PluginUtil.kt b/src/main/kotlin/il/co/sysbind/intellij/moodledev/util/PluginUtil.kt new file mode 100644 index 0000000..5a78e89 --- /dev/null +++ b/src/main/kotlin/il/co/sysbind/intellij/moodledev/util/PluginUtil.kt @@ -0,0 +1,12 @@ +package il.co.sysbind.intellij.moodledev.util + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.extensions.PluginId + +object PluginUtil { + fun isPluginInstalled(id: String): Boolean = try { + PluginManagerCore.isPluginInstalled(PluginId.getId(id)) + } catch (_: Throwable) { + false + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 74132dc..f59a60a 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -2,7 +2,7 @@ il.co.sysbind.intellij.moodledev Moodle Development - 2.2.0 + 2.2.1 SysBind Plugin For Moodle Developers @@ -40,6 +40,8 @@ + + diff --git a/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiLlmConfiguratorTest.kt b/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiLlmConfiguratorTest.kt new file mode 100644 index 0000000..3476260 --- /dev/null +++ b/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiLlmConfiguratorTest.kt @@ -0,0 +1,47 @@ +package il.co.sysbind.intellij.moodledev.ai + +import com.intellij.testFramework.LightPlatformTestCase +import com.intellij.testFramework.ServiceContainerUtil +import il.co.sysbind.intellij.moodledev.project.MoodleProjectSettings +import il.co.sysbind.intellij.moodledev.project.MoodleSettings +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue + +class MoodleAiLlmConfiguratorTest : LightPlatformTestCase() { + + private class TestConfigurator(private val aiPresent: Boolean) : MoodleAiLlmConfigurator() { + var appliedCalled = false + override fun isAiPluginInstalled(): Boolean = aiPresent + override fun applyAIPrompts() { + appliedCalled = true + } + } + + private fun registerSettings(pluginEnabled: Boolean) { + val svc = MoodleProjectSettings() + svc.settings = MoodleSettings().also { it.pluginEnabled = pluginEnabled } + ServiceContainerUtil.replaceService(project, MoodleProjectSettings::class.java, svc, testRootDisposable) + } + + fun testNotEnabled_skipsEverything() { + registerSettings(pluginEnabled = false) + val cfg = TestConfigurator(aiPresent = true) + // Should not throw and should not apply + runCatching { cfg.execute(project) } + assertFalse(cfg.appliedCalled, "applyAIPrompts should not be called when plugin is disabled") + } + + fun testEnabledButAiPluginMissing_skipsApply() { + registerSettings(pluginEnabled = true) + val cfg = TestConfigurator(aiPresent = false) + runCatching { cfg.execute(project) } + assertFalse(cfg.appliedCalled, "applyAIPrompts should not be called when AI plugin missing") + } + + fun testEnabledAndAiPluginPresent_applies() { + registerSettings(pluginEnabled = true) + val cfg = TestConfigurator(aiPresent = true) + runCatching { cfg.execute(project) }.onFailure { throw it } + assertTrue(cfg.appliedCalled, "applyAIPrompts should be called when enabled and AI plugin present") + } +} diff --git a/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiPromptsTest.kt b/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiPromptsTest.kt new file mode 100644 index 0000000..ba8823e --- /dev/null +++ b/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiPromptsTest.kt @@ -0,0 +1,22 @@ +package il.co.sysbind.intellij.moodledev.ai + +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class MoodleAiPromptsTest { + @Test + fun `php doc prompt mentions Moodle coding style and PHPDoc`() { + val p = MoodleAiPrompts.phpDocPrompt + assertTrue(p.contains("moodledev.io/general/development/policies/codingstyle")) + assertTrue(p.contains("PHPDoc")) + assertTrue(p.contains("@covers") || p.contains("@cover")) + } + + @Test + fun `commit message prompt includes structure and example`() { + val p = MoodleAiPrompts.commitMessagePrompt + assertTrue(p.contains("<\$GIT_BRANCH_NAME> : ")) + assertTrue(p.contains("issue7685 mod_quiz")) + assertTrue(p.contains("First line should be no more than 72 characters")) + } +} diff --git a/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiUiTest.kt b/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiUiTest.kt new file mode 100644 index 0000000..5011514 --- /dev/null +++ b/src/test/kotlin/il/co/sysbind/intellij/moodledev/ai/MoodleAiUiTest.kt @@ -0,0 +1,31 @@ +package il.co.sysbind.intellij.moodledev.ai + +import com.intellij.testFramework.LightPlatformTestCase +import com.intellij.testFramework.ServiceContainerUtil +import il.co.sysbind.intellij.moodledev.project.MoodleProjectSettings +import il.co.sysbind.intellij.moodledev.project.MoodleSettings + +/** + * A lightweight UI-ish test that runs inside the IntelliJ test environment and ensures + * the MoodleAiLlmConfigurator executes without throwing when the plugin is enabled. + * This does not require the AI plugin to be installed in tests. + */ +class MoodleAiUiTest : LightPlatformTestCase() { + private class NoOpConfigurator : MoodleAiLlmConfigurator() { + var executed = false + override fun isAiPluginInstalled(): Boolean = false // simulate missing AI plugin + override fun applyAIPrompts() { executed = true } + } + + fun testProjectActivityRunsSafelyWhenPluginEnabled() { + val svc = MoodleProjectSettings() + svc.settings = MoodleSettings().also { it.pluginEnabled = true } + ServiceContainerUtil.replaceService(project, MoodleProjectSettings::class.java, svc, testRootDisposable) + + val cfg = NoOpConfigurator() + // Should complete without exceptions; with AI plugin missing it should skip applyAIPrompts + runCatching { cfg.execute(project) }.onFailure { throw it } + // Since AI plugin missing, prompts should not be applied + assertFalse("applyAIPrompts should not be called without AI plugin") { cfg.executed } + } +}