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
1 change: 1 addition & 0 deletions .junie/guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ This is an IntelliJ Platform plugin project for Moodle development support.
```

## Testing
- Follow [Testing Docs](https://plugins.jetbrains.com/docs/intellij/testing-plugins.html) and his links to learn more about testing and writing tests.
- Run tests:
```bash
./gradlew test
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0).

## [Unreleased]

### Added
- Unit and UI tests to verify Composer "Synchronize IDE settings with composer.json" is disabled when enabling the Moodle framework.

## [2.2.1] - 2025-10-16

### Fixed
- When enabling the Moodle framework, the plugin now programmatically disables PHP > Composer > "Synchronize IDE settings with composer.json" to prevent unintended overwrites of IDE configuration.

### Added

- Bundled Moodle inspection profile and registered it in `plugin.xml` so it becomes available after plugin installation.
Expand Down Expand Up @@ -222,7 +230,8 @@ Add support for PHPStorm 2022.2
- Add live Template for Moodle $ADMIN by type ADMIN
- Add Moodle code style for predefined code styles for PHP/Javascript/SCSS/LESS

[Unreleased]: https://github.com/SysBind/moodle-dev/compare/2.2.0...HEAD
[Unreleased]: https://github.com/SysBind/moodle-dev/compare/2.2.1...HEAD
[2.2.1]: https://github.com/SysBind/moodle-dev/compare/2.2.0...2.2.1
[2.2.0]: https://github.com/SysBind/moodle-dev/compare/2.1.1...2.2.0
[2.1.1]: https://github.com/SysBind/moodle-dev/compare/2.1.0...2.1.1
[2.1.0]: https://github.com/SysBind/moodle-dev/compare/v2.0.0...2.1.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Moodle Development Plugin for IntelliJ IDEA
ull# Moodle Development Plugin for IntelliJ IDEA

![Build](https://github.com/SysBind/moodle-dev/workflows/Build/badge.svg)
![Version](https://img.shields.io/jetbrains/plugin/v/16702)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ class MoodleSettingsForm(val project: Project) : PhpFrameworkConfigurable {
settings.userName = userName.component.text
settings.userEmail = userEmail.component.text

// If Moodle framework is enabled, ensure Composer IDE sync is disabled
if (settings.pluginEnabled) {
try {
il.co.sysbind.intellij.moodledev.util.PhpComposerSettingsUtil.disableComposerSync(project)
} catch (t: Throwable) {
log.warn("Unable to disable Composer sync via utility: ${t.message}")
}
}

// Configure PHP_Codesniffer if plugin is enabled
if (settings.pluginEnabled) {
// Check if composer is available
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package il.co.sysbind.intellij.moodledev.util

import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project

/**
* Utilities for interacting with the PHP Composer settings without taking a hard compile-time dependency
* on internal APIs that may change between PHP plugin versions.
*
* Goal: ensure "Synchronize IDE settings with composer.json" is disabled when Moodle framework is enabled.
*/
object PhpComposerSettingsUtil {
private val log = Logger.getInstance(PhpComposerSettingsUtil::class.java)

/**
* Try to disable Composer auto-synchronization of IDE settings with composer.json.
* Uses reflection to remain compatible across PHP plugin versions where the API name may differ.
*/
fun disableComposerSync(project: Project) {
try {
// Try primary known settings class
val clazz = try {
Class.forName("com.jetbrains.php.composer.ComposerSettings")
} catch (cnf: ClassNotFoundException) {
// Fallback: some versions might use different package/name; keep room for expansion
log.warn("ComposerSettings class not found: ${cnf.message}")
null
} ?: return

// Obtain getInstance(Project) if available, else fall back to no-arg getInstance()
val instance = try {
val withProject = clazz.methods.firstOrNull {
it.name.equals("getInstance", true) && it.parameterCount == 1 && Project::class.java.isAssignableFrom(it.parameterTypes[0])
}
val noArg = clazz.methods.firstOrNull { it.name.equals("getInstance", true) && it.parameterCount == 0 }
when {
withProject != null -> withProject.invoke(null, project)
noArg != null -> noArg.invoke(null)
else -> null
}
} catch (e: Exception) {
log.warn("Failed to obtain ComposerSettings instance: ${e.message}")
null
} ?: return

// 1) Prefer enum-based API: setSynchronizationState(SynchronizationState.DONT_SYNCHRONIZE)
try {
val enumClass = try {
Class.forName("com.jetbrains.php.composer.SynchronizationState")
} catch (e: ClassNotFoundException) {
null
}
val setState = clazz.methods.firstOrNull { m ->
m.name.equals("setSynchronizationState", true) && m.parameterCount == 1 && (enumClass == null || m.parameterTypes[0].isEnum)
}
if (setState != null) {
val dont = enumClass?.enumConstants?.firstOrNull { (it as Enum<*>).name.equals("DONT_SYNCHRONIZE", true) }
?: setState.parameterTypes[0].enumConstants.firstOrNull { (it as Enum<*>).name.contains("DONT", true) }
if (dont != null) {
setState.isAccessible = true
setState.invoke(instance, dont)
log.info("Disabled Composer sync via setSynchronizationState(DONT_SYNCHRONIZE).")
return
}
}
} catch (ignore: Throwable) {
// fall through to boolean-based API
}

// 2) Boolean-based API fallbacks: look for setter containing "sync"
val candidates = clazz.methods.filter { method ->
method.name.startsWith("set") &&
method.parameterCount == 1 &&
(method.parameterTypes[0] == java.lang.Boolean.TYPE || method.parameterTypes[0] == java.lang.Boolean::class.java) &&
method.name.contains("sync", ignoreCase = true)
}

// If no obvious candidates, try some well-known names explicitly to be safe in future refactors
val preferredNames = listOf(
"setSynchronizeWithComposerJson",
"setSynchronizeIdeSettingsWithComposerJson",
"setSyncWithComposerJson",
"setAutoSyncEnabled",
"setSynchronizeSettings"
)

val method = candidates.firstOrNull { m -> preferredNames.any { pn -> m.name.equals(pn, ignoreCase = true) } }
?: candidates.firstOrNull()

if (method != null) {
method.isAccessible = true
method.invoke(instance, false)
log.info("Disabled PHP Composer synchronization with composer.json via ${method.name}().")
} else {
log.warn("Could not find a suitable Composer sync setting method to invoke.")
}
} catch (t: Throwable) {
log.warn("Failed to disable Composer sync: ${t.message}")
}
}
}
2 changes: 1 addition & 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
56 changes: 56 additions & 0 deletions src/test/kotlin/com/jetbrains/php/composer/ComposerSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.jetbrains.php.composer

import com.intellij.openapi.project.Project

/**
* Test double for the PHP plugin's ComposerSettings class.
* Provides a minimal surface for PhpComposerSettingsUtil to reflect on.
*/
class ComposerSettings private constructor(val project: Project? = null) {
var synchronizeWithComposerJson: Boolean = true
var synchronizationState: SynchronizationState = SynchronizationState.SYNCHRONIZE

// Enum mirrors real plugin concept for workspace.xml persistence
enum class SynchronizationState { SYNCHRONIZE, DONT_SYNCHRONIZE }

// Preferred newer API
fun setSynchronizationState(state: SynchronizationState) {
synchronizationState = state
// Keep boolean flag consistent with enum semantics
synchronizeWithComposerJson = state == SynchronizationState.SYNCHRONIZE
}

// Alternative setter name to ensure our reflection fallback remains covered
fun setAutoSyncEnabled(value: Boolean) {
synchronizeWithComposerJson = value
synchronizationState = if (value) SynchronizationState.SYNCHRONIZE else SynchronizationState.DONT_SYNCHRONIZE
}

companion object {
@JvmStatic
private var lastInstance: ComposerSettings? = null

@JvmStatic
fun getInstance(project: Project): ComposerSettings {
val inst = ComposerSettings(project)
lastInstance = inst
return inst
}

@JvmStatic
fun getInstance(): ComposerSettings {
val inst = ComposerSettings(null)
lastInstance = inst
return inst
}

@JvmStatic
fun getLastInstance(): ComposerSettings? = lastInstance

// Test-only utility to reset singleton-like state between tests
@JvmStatic
fun clearLastInstanceForTests() {
lastInstance = null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package il.co.sysbind.intellij.moodledev.project

import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.jetbrains.php.composer.ComposerSettings
import il.co.sysbind.intellij.moodledev.util.PhpComposerSettingsUtil
import org.junit.Test

class MoodleComposerSyncEnumTest : BasePlatformTestCase() {

@Test
fun testDisableComposerSync_UsesEnumWhenAvailable() {
// Precondition: test double provides enum-based API
ComposerSettings.clearLastInstanceForTests()

// Act
PhpComposerSettingsUtil.disableComposerSync(project)

// Assert
val settings = ComposerSettings.getLastInstance()
assertNotNull("ComposerSettings should have been instantiated via reflection", settings)
assertEquals(
"SynchronizationState should be DONT_SYNCHRONIZE",
ComposerSettings.SynchronizationState.DONT_SYNCHRONIZE,
settings!!.synchronizationState
)
assertFalse("Boolean mirror should be false when enum is DONT_SYNCHRONIZE", settings.synchronizeWithComposerJson)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package il.co.sysbind.intellij.moodledev.project

import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.jetbrains.php.composer.ComposerSettings
import il.co.sysbind.intellij.moodledev.MoodleBundle
import org.junit.Test

class MoodleSettingsComposerSyncTest : BasePlatformTestCase() {

@Test
fun testApply_DisablesComposerSyncWhenFrameworkEnabled() {
// Arrange
val projectSettings = project.getService(MoodleProjectSettings::class.java)
projectSettings.settings.pluginEnabled = false
val form = MoodleSettingsForm(project)
// create UI to initialize components
val component = form.createComponent()
assertNotNull("Settings form component should be created", component)
// Ensure initial state false
assertFalse("Precondition: plugin should be disabled", form.pluginEnabled.component.isSelected)

// Act: user enables the framework and clicks Apply
form.pluginEnabled.component.isSelected = true
form.apply()

// Assert: state persisted and composer sync disabled via test double
assertTrue("Plugin should be enabled after apply", projectSettings.settings.pluginEnabled)
val instance = ComposerSettings.getLastInstance()
assertNotNull("ComposerSettings test double should have been instantiated", instance)
assertFalse("Composer sync should be disabled when enabling framework", instance!!.synchronizeWithComposerJson)

// UI bits sanity: display name/id available (lightweight UI test)
assertTrue(MoodleBundle.getMessage("configurable.name").isNotBlank())
assertTrue(form.getId().isNotBlank())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package il.co.sysbind.intellij.moodledev.project

import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.jetbrains.php.composer.ComposerSettings
import org.junit.Test

/**
* Lightweight UI-ish test exercising the Configurable form apply() path.
* It simulates a user enabling the Moodle framework via the Settings page
* and verifies that Composer sync is disabled using our reflection bridge.
*/
class MoodleSettingsUiToggleTest : BasePlatformTestCase() {

@Test
fun testEnablingFrameworkDisablesComposerSync_Idempotent() {
// Arrange
val form = MoodleSettingsForm(project)
form.createComponent() // initialize form components

// Enable and apply twice to ensure idempotency
form.pluginEnabled.component.isSelected = true
form.apply()
form.apply()

val cs = ComposerSettings.getLastInstance()
assertNotNull("ComposerSettings test instance should exist after apply()", cs)
assertFalse("Composer sync should be disabled after enabling framework",
cs!!.synchronizeWithComposerJson)
assertEquals("Enum state should be DONT_SYNCHRONIZE",
ComposerSettings.SynchronizationState.DONT_SYNCHRONIZE,
cs.synchronizationState)

// Now disable framework and apply; we do not re-enable sync automatically.
form.pluginEnabled.component.isSelected = false
form.apply()

// Composer sync should remain disabled (plugin does not auto-enable it on disable)
val cs2 = ComposerSettings.getLastInstance()
assertFalse("Composer sync should remain disabled after disabling framework",
cs2!!.synchronizeWithComposerJson)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package il.co.sysbind.intellij.moodledev.util

import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.jetbrains.php.composer.ComposerSettings
import org.junit.Test

class PhpComposerSettingsUtilTest : BasePlatformTestCase() {

override fun setUp() {
super.setUp()
// Reset shared state of the fake ComposerSettings between tests
ComposerSettings.clearLastInstanceForTests()
}

@Test
fun testDisableComposerSync_DisablesOnFakeComposerSettings() {
// Ensure there's no lingering instance from other tests
val before = ComposerSettings.getLastInstance()
assertNull("Precondition: last instance should be null or irrelevant", before)

// Act
PhpComposerSettingsUtil.disableComposerSync(project)

// Assert
val instance = ComposerSettings.getLastInstance()
assertNotNull("ComposerSettings instance should be created by reflection", instance)
assertFalse(
"Composer synchronization must be disabled when called",
instance!!.synchronizeWithComposerJson
)
}
}