Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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).")
}
}
}
Original file line number Diff line number Diff line change
@@ -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> <code_area>: <short_summary>")
appendLine()
appendLine("<detailed_explanation>")
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")
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 3 additions & 1 deletion src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<idea-plugin>
<id>il.co.sysbind.intellij.moodledev</id>
<name>Moodle Development</name>
<version>2.2.0</version>
<version>2.2.1</version>
<vendor email="support@sysbind.co.il" url="https://sysbin.co.il">SysBind</vendor>
<description><![CDATA[
<h1>Plugin For Moodle Developers</h1>
Expand Down Expand Up @@ -40,6 +40,8 @@
<predefinedCodeStyle implementation="il.co.sysbind.intellij.moodledev.codeStyle.MoodleScssPredefinedCodeStyle"/>
<bundledInspectionProfile path="/inspectionProfiles/Moodle" id="Moodle.InspectProfile"/>

<projectActivity implementation="il.co.sysbind.intellij.moodledev.ai.MoodleAiLlmConfigurator"/>

<internalFileTemplate name="Moodle PHP File"/>
<internalFileTemplate name="Moodle PHP Class"/>
<internalFileTemplate name="Moodle PHP Interface"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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> <code_area>: <short_summary>"))
assertTrue(p.contains("issue7685 mod_quiz"))
assertTrue(p.contains("First line should be no more than 72 characters"))
}
}
Original file line number Diff line number Diff line change
@@ -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 }
}
}