package cc.unitmesh.devins.idea.compose import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.size import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import cc.unitmesh.devins.idea.services.CoroutineScopeHolder import com.intellij.openapi.application.EDT import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.jetbrains.jewel.foundation.theme.JewelTheme import javax.swing.Timer /** * Compose effect utilities that avoid ClassLoader conflicts with IntelliJ's bundled Compose. * * These utilities replace Compose's built-in coroutine APIs (LaunchedEffect, rememberCoroutineScope, * collectAsState) which cause ClassLoader conflicts because IntelliJ's Compose runtime expects * CoroutineScope from its own ClassLoader, but the plugin provides one from a different ClassLoader. * * Solution: Use DisposableEffect (which doesn't use coroutines internally) combined with * IntelliJ's CoroutineScopeHolder service. */ // Application-level fallback scope for when no project is available // Uses Dispatchers.EDT (IntelliJ's EDT dispatcher) to ensure EDT execution for Compose animations private val appScope: CoroutineScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.EDT) } /** * Gets a coroutine scope from the project's CoroutineScopeHolder service, * or falls back to an application-level scope if no project is provided. */ private fun getScope(project: Project?): CoroutineScope { return if (project != null && !project.isDisposed) { project.service().createScope("IdeaComposeEffect") } else { appScope } } /** * Replacement for rememberCoroutineScope() that uses IntelliJ's coroutine scope. * This avoids ClassLoader conflicts with Compose's internal coroutine usage. */ @Composable fun rememberIdeaCoroutineScope(project: Project? = null): CoroutineScope { return remember(project) { getScope(project) } } /** * Replacement for LaunchedEffect that uses IntelliJ's coroutine scope. * Uses DisposableEffect internally to avoid ClassLoader conflicts. * * @param key1 Key that triggers re-execution when changed * @param project Optional project for project-scoped coroutines * @param block The suspend block to execute */ @Composable fun IdeaLaunchedEffect( key1: Any?, project: Project? = null, block: suspend CoroutineScope.() -> Unit ) { val scope = getScope(project) DisposableEffect(key1) { val job = scope.launch { block() } onDispose { job.cancel() } } } /** * Replacement for LaunchedEffect with two keys. */ @Composable fun IdeaLaunchedEffect( key1: Any?, key2: Any?, project: Project? = null, block: suspend CoroutineScope.() -> Unit ) { val scope = getScope(project) DisposableEffect(key1, key2) { val job = scope.launch { block() } onDispose { job.cancel() } } } /** * Replacement for LaunchedEffect with three keys. */ @Composable fun IdeaLaunchedEffect( key1: Any?, key2: Any?, key3: Any?, project: Project? = null, block: suspend CoroutineScope.() -> Unit ) { val scope = getScope(project) DisposableEffect(key1, key2, key3) { val job = scope.launch { block() } onDispose { job.cancel() } } } /** * Custom CircularProgressIndicator that avoids ClassLoader conflicts. * * Jewel's CircularProgressIndicator uses Dispatchers.getDefault() internally, * which causes ClassLoader conflicts between the plugin's coroutines and * IntelliJ's bundled Compose runtime. * * This implementation uses a Swing Timer for animation instead of coroutines. */ @Composable fun IdeaCircularProgressIndicator( modifier: Modifier = Modifier, size: Dp = 16.dp, strokeWidth: Dp = 2.dp, color: Color? = null ) { var rotation by remember { mutableStateOf(0f) } // Use Swing Timer instead of coroutines to avoid ClassLoader conflicts DisposableEffect(Unit) { val timer = Timer(16) { // ~60fps rotation = (rotation + 6f) % 360f } timer.start() onDispose { timer.stop() } } val progressColor = color ?: JewelTheme.globalColors.text.info Canvas(modifier = modifier.size(size)) { val strokePx = strokeWidth.toPx() val arcSize = Size(this.size.width - strokePx, this.size.height - strokePx) val topLeft = Offset(strokePx / 2, strokePx / 2) // Draw spinning arc drawArc( color = progressColor, startAngle = rotation, sweepAngle = 270f, useCenter = false, topLeft = topLeft, size = arcSize, style = Stroke(width = strokePx, cap = StrokeCap.Round) ) } }