diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c7b6e..07750ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,18 @@ ## [Unreleased] +## [0.7.3] - 2024-03-02 + +### Added + +- [Svelte/Vue] Notify when an updated component does no longer uses some installed files, allowing to remove them +- Allow to remove installed dependencies when they are no longer used by any component + +### Fixed + +- Fix a freeze when executing commands on macOS/Linux +- Improve performance and stability + ## [0.7.2] - 2024-02-16 ### Fixed diff --git a/gradle.properties b/gradle.properties index 52b85b5..acbd471 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.warningimhack3r.intellijshadcnplugin pluginName = intellij-shadcn-plugin pluginRepositoryUrl = https://github.com/WarningImHack3r/intellij-shadcn-plugin # SemVer format -> https://semver.org -pluginVersion = 0.7.2 +pluginVersion = 0.7.3 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 213 diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/DependencyManager.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/DependencyManager.kt index 063870e..055e964 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/DependencyManager.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/DependencyManager.kt @@ -14,50 +14,78 @@ class DependencyManager(private val project: Project) { PROD } - private fun installDependency(dependencyName: String, installationType: InstallationType = InstallationType.PROD) { - // get the package manager + private fun getPackageManager(): String? { val fileManager = FileManager(project) - val packageManager = mapOf( + return mapOf( "package-lock.json" to "npm", "pnpm-lock.yaml" to "pnpm", "yarn.lock" to "yarn", "bun.lockb" to "bun" - ).filter { runReadAction { - fileManager.getVirtualFilesByName(it.key).isNotEmpty() - } }.values.firstOrNull() - // install the dependency - val command = listOfNotNull( - packageManager, - "i", - if (installationType == InstallationType.DEV) "-D" else null, - dependencyName - ).toTypedArray() - val res = ShellRunner(project).execute(command) - // check if the installation was successful - if (res == null) { - NotificationManager(project).sendNotification( - "Failed to install dependency $dependencyName", - "Failed to install dependency $dependencyName (${command.joinToString(" ")}). Please install it manually.", - NotificationType.ERROR - ) - } + ).filter { + runReadAction { + fileManager.getVirtualFilesByName(it.key).isNotEmpty() + } + }.values.firstOrNull() } fun installDependencies(dependencyNames: List, installationType: InstallationType = InstallationType.PROD) { - dependencyNames.forEach { installDependency(it, installationType) } + getPackageManager()?.let { packageManager -> + // install the dependency + val command = listOfNotNull( + packageManager, + "i", + if (installationType == InstallationType.DEV) "-D" else null, + *dependencyNames.toTypedArray() + ).toTypedArray() + val res = ShellRunner(project).execute(command) + // check if the installation was successful + if (res == null) { + NotificationManager(project).sendNotification( + "Failed to install dependencies", + "Failed to install dependencies: ${dependencyNames.joinToString { ", " }} (${command.joinToString(" ")}). Please install it manually.", + NotificationType.ERROR + ) + } + } ?: throw IllegalStateException("No package manager found") } - fun isDependencyInstalled(dependency: String): Boolean { + fun uninstallDependencies(dependencyNames: List) { + getPackageManager()?.let { packageManager -> + // uninstall the dependencies + val command = listOf( + packageManager, + "remove", + *dependencyNames.toTypedArray() + ).toTypedArray() + val res = ShellRunner(project).execute(command) + // check if the uninstallation was successful + if (res == null) { + NotificationManager(project).sendNotification( + "Failed to uninstall dependencies", + "Failed to uninstall dependencies (${command.joinToString(" ")}). Please uninstall them manually.", + NotificationType.ERROR + ) + } + } ?: throw IllegalStateException("No package manager found") + } + + fun getInstalledDependencies(): List { // Read the package.json file return FileManager(project).getFileContentsAtPath("package.json")?.let { packageJson -> Json.parseToJsonElement(packageJson).jsonObject.filter { it.key == "dependencies" || it.key == "devDependencies" }.map { it.value.jsonObject.keys }.flatten().also { - logger().debug("Installed dependencies: $it, is $dependency installed? ${it.contains(dependency)}") - }.contains(dependency) - // Check if the dependency is installed - } ?: false.also { - logger().error("package.json not found, returning false") + logger().debug("Installed dependencies: $it") + } + } ?: emptyList().also { + logger().error("package.json not found") + } + } + + fun isDependencyInstalled(dependency: String): Boolean { + // Read the package.json file + return getInstalledDependencies().contains(dependency).also { + logger().debug("Is $dependency installed? $it") } } } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/FileManager.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/FileManager.kt index dd61c66..f94b8d9 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/FileManager.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/FileManager.kt @@ -25,14 +25,20 @@ class FileManager(private val project: Project) { } } - fun deleteFileAtPath(path: String): Boolean { + fun deleteFile(file: VirtualFile): Boolean { return try { - getFileAtPath(path)?.delete(this)?.let { true } ?: false + file.delete(this).let { true } } catch (e: IOException) { false }.also { - if (!it) log.warn("Unable to delete file at path $path") - else log.debug("Deleted file at path $path") + if (!it) log.warn("Unable to delete file at path ${file.path}") + else log.debug("Deleted file at path ${file.path}") + } + } + + fun deleteFileAtPath(path: String): Boolean { + return getFileAtPath(path)?.let { deleteFile(it) } ?: false.also { + log.warn("No file to delete found at path $path") } } @@ -60,7 +66,9 @@ class FileManager(private val project: Project) { name, GlobalSearchScope.projectScope(project) ) - }).sortedBy { file -> + }).filter { file -> + !file.path.contains("/node_modules/") && !file.path.contains("/.git/") + }.sortedBy { file -> name.toRegex().find(file.path)?.range?.first ?: Int.MAX_VALUE }.also { log.debug("Found ${it.size} files named $name: ${it.toList()}") @@ -69,9 +77,7 @@ class FileManager(private val project: Project) { private fun getDeepestFileForPath(filePath: String): VirtualFile { var paths = filePath.split('/') - var currentFile = getVirtualFilesByName(paths.first()).firstOrNull() ?: throw NoSuchFileException("No file found at path $filePath").also { - log.warn("No file found at path ${paths.first()}") - } + var currentFile = getVirtualFilesByName(paths.first()).firstOrNull() ?: throw NoSuchFileException("No file found at path $filePath") paths = paths.drop(1) for (path in paths) { val child = currentFile.findChild(path) diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/ShellRunner.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/ShellRunner.kt index bd4321e..e2eea4a 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/ShellRunner.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/ShellRunner.kt @@ -21,8 +21,9 @@ class ShellRunner(private val project: Project? = null) { val platformCommand = if (isWindows()) { arrayOf("cmd", "/c") } else { - arrayOf("sh", "-c") + emptyArray() } + command + log.debug("Executing command: \"${platformCommand.joinToString(" ")}\"") val process = ProcessBuilder(*platformCommand) .redirectOutput(ProcessBuilder.Redirect.PIPE) .directory(project?.basePath?.let { File(it) }) @@ -33,6 +34,7 @@ class ShellRunner(private val project: Project? = null) { } } catch (e: Exception) { if (isWindows() && !commandName.endsWith(".cmd")) { + log.warn("Failed to execute \"${command.joinToString(" ")}\". Trying to execute \"$commandName.cmd\" instead", e) failedCommands.add(commandName) return execute(arrayOf("$commandName.cmd") + command.drop(1).toTypedArray()) } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt index b8f2b07..9559298 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt @@ -8,6 +8,7 @@ import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.remote.Co import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.remote.ComponentWithContents import com.github.warningimhack3r.intellijshadcnplugin.notifications.NotificationManager import com.intellij.notification.NotificationAction +import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.fileTypes.FileTypeManager import com.intellij.openapi.project.Project @@ -61,18 +62,14 @@ abstract class Source(val project: Project, private val serializer: private fun fetchComponent(componentName: String): ComponentWithContents { val style = getLocalConfig().style val response = RequestSender.sendRequest("$domain/registry/styles/$style/$componentName.json") - return response.ok { Json.decodeFromString(it.body) } ?: throw Exception("Component not found").also { - log.error("Unable to fetch component $componentName", it) - } + return response.ok { Json.decodeFromString(it.body) } ?: throw Exception("Component $componentName not found") } protected fun fetchColors(): JsonElement { val baseColor = getLocalConfig().tailwind.baseColor return RequestSender.sendRequest("$domain/registry/colors/$baseColor.json").ok { Json.parseToJsonElement(it.body) - } ?: throw Exception("Colors not found").also { - log.error("Unable to fetch colors", it) - } + } ?: throw Exception("Colors not found") } protected open fun getRegistryDependencies(component: ComponentWithContents): List { @@ -83,12 +80,12 @@ abstract class Source(val project: Project, private val serializer: } // Public methods - open fun fetchAllComponents(): List { + open fun fetchAllComponents(): List { return RequestSender.sendRequest("$domain/registry/index.json").ok { Json.decodeFromString>(it.body) - }?.map { ISPComponent(it.name) }?.also { + }?.also { log.info("Fetched ${it.size} remote components: ${it.joinToString(", ") { component -> component.name }}") - } ?: emptyList().also { + } ?: emptyList().also { log.error("Unable to fetch remote components") } } @@ -109,12 +106,55 @@ abstract class Source(val project: Project, private val serializer: // Install component val component = fetchComponent(componentName) val installedComponents = getInstalledComponents() + val fileManager = FileManager(project) + val notifManager = NotificationManager(project) log.debug("Installing ${component.name} (installed: ${installedComponents.joinToString(", ")})") setOf(component, *getRegistryDependencies(component).filter { !installedComponents.contains(it.name) }.toTypedArray()).also { log.debug("Installing ${it.size} components: ${it.joinToString(", ") { component -> component.name }}") }.forEach { downloadedComponent -> + val path = + "${resolveAlias(getLocalConfig().aliases.components)}/${component.type.substringAfterLast(":")}" + if (usesDirectoriesForComponents()) { + "/${downloadedComponent.name}" + } else "" + if (usesDirectoriesForComponents()) { + val remotelyDeletedFiles = fileManager.getFileAtPath(path)?.children?.filter { file -> + downloadedComponent.files.none { it.name == file.name } + } ?: emptyList() + val multipleFiles = remotelyDeletedFiles.size > 1 + notifManager.sendNotification( + "Deprecated component file${if (multipleFiles) "s" else ""} in ${downloadedComponent.name}", + "The following file${if (multipleFiles) "s are" else " is"} no longer part of ${downloadedComponent.name}: ${ + remotelyDeletedFiles.joinToString(", ") { file -> + file.name + } + }. Do you want to remove ${if (multipleFiles) "them" else "it"}?" + ) { notification -> + listOf( + NotificationAction.createSimple("Remove " + if (multipleFiles) "them" else "it") { + remotelyDeletedFiles.forEach { file -> + runWriteAction { fileManager.deleteFile(file) } + } + log.info( + "Removed deprecated file${if (multipleFiles) "s" else ""} from ${downloadedComponent.name} (${ + downloadedComponent.files.joinToString(", ") { file -> + file.name + } + }): ${ + remotelyDeletedFiles.joinToString(", ") { file -> + file.name + } + }" + ) + notification.expire() + }, + NotificationAction.createSimple("Keep " + if (multipleFiles) "them" else "it") { + notification.expire() + } + ) + } + } downloadedComponent.files.forEach { file -> val psiFile = PsiFileFactory.getInstance(project).createFileFromText( adaptFileExtensionToConfig(file.name), @@ -123,10 +163,7 @@ abstract class Source(val project: Project, private val serializer: ), adaptFileToConfig(file.content) ) - val path = "${resolveAlias(getLocalConfig().aliases.components)}/${component.type.substringAfterLast(":")}" + if (usesDirectoriesForComponents()) { - "/${downloadedComponent.name}" - } else "" - FileManager(project).saveFileAtPath(psiFile, path) + fileManager.saveFileAtPath(psiFile, path) } } @@ -142,7 +179,6 @@ abstract class Source(val project: Project, private val serializer: "${dropLast(1).joinToString(", ")} and ${last()}" } } - val notifManager = NotificationManager(project) notifManager.sendNotification( "Installed ${component.name}", "${component.name} requires $dependenciesList to be installed." @@ -170,9 +206,11 @@ abstract class Source(val project: Project, private val serializer: val remoteComponent = fetchComponent(componentName) return remoteComponent.files.all { file -> (FileManager(project).getFileContentsAtPath( - "${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}${if (usesDirectoriesForComponents()) { - "/${remoteComponent.name}" - } else ""}/${file.name}" + "${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}${ + if (usesDirectoriesForComponents()) { + "/${remoteComponent.name}" + } else "" + }/${file.name}" ) == adaptFileToConfig(file.content)).also { log.debug("File ${file.name} for ${remoteComponent.name} is ${if (it) "" else "NOT "}up to date") } @@ -181,7 +219,8 @@ abstract class Source(val project: Project, private val serializer: open fun removeComponent(componentName: String) { val remoteComponent = fetchComponent(componentName) - val componentsDir = "${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}" + val componentsDir = + "${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}" if (usesDirectoriesForComponents()) { FileManager(project).deleteFileAtPath("$componentsDir/${remoteComponent.name}") } else { @@ -189,5 +228,40 @@ abstract class Source(val project: Project, private val serializer: FileManager(project).deleteFileAtPath("$componentsDir/${file.name}") } } + // Remove dependencies no longer needed by any component + val remoteComponents = fetchAllComponents() + val allPossiblyNeededDependencies = remoteComponents.map { it.dependencies }.flatten().toSet() + val currentlyNeededDependencies = getInstalledComponents().map { component -> + remoteComponents.find { it.name == component }?.dependencies ?: emptyList() + }.flatten().toSet() + val uselessDependencies = DependencyManager(project).getInstalledDependencies().filter { dependency -> + dependency in allPossiblyNeededDependencies && dependency !in currentlyNeededDependencies + } + if (uselessDependencies.isNotEmpty()) { + val multipleDependencies = uselessDependencies.size > 1 + val notifManager = NotificationManager(project) + notifManager.sendNotification( + "Unused dependenc${if (multipleDependencies) "ies" else "y"} found", + "The following dependenc${if (multipleDependencies) "ies are" else "y is"} no longer needed by any component: ${ + uselessDependencies.joinToString(", ") { + it + } + }. Do you want to remove ${if (multipleDependencies) "them" else "it"}?" + ) { notif -> + listOf( + NotificationAction.createSimple("Remove") { + runAsync { + DependencyManager(project).uninstallDependencies(uselessDependencies) + }.then { + notifManager.sendNotificationAndHide( + "Removed dependenc${if (multipleDependencies) "ies" else "y"}", + "Removed ${uselessDependencies.joinToString(", ") { it }}." + ) + } + notif.expire() + } + ) + } + } } } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/ReactSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/ReactSource.kt index bab6773..8dcf078 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/ReactSource.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/ReactSource.kt @@ -23,17 +23,13 @@ class ReactSource(project: Project) : Source(project, ReactConfig.s log.debug("Alias $alias does not start with $ or @, returning it as-is") } val configFile = if (getLocalConfig().tsx) "tsconfig.json" else "jsconfig.json" - val tsConfig = FileManager(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found").also { - log.error("Failed to get $configFile, throwing exception") - } + val tsConfig = FileManager(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found") val aliasPath = Json.parseToJsonElement(tsConfig) .jsonObject["compilerOptions"] ?.jsonObject?.get("paths") ?.jsonObject?.get("${alias.substringBefore("/")}/*") ?.jsonArray?.get(0) - ?.jsonPrimitive?.content ?: throw Exception("Cannot find alias $alias").also { - log.error("Failed to find alias $alias in $tsConfig, throwing exception") - } + ?.jsonPrimitive?.content ?: throw Exception("Cannot find alias $alias in $tsConfig") return aliasPath.replace(Regex("^\\.+/"), "") .replace(Regex("\\*$"), alias.substringAfter("/")).also { log.debug("Resolved alias $alias to $it") diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidSource.kt index ceb592d..ee9fee1 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidSource.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidSource.kt @@ -22,17 +22,13 @@ class SolidSource(project: Project) : Source(project, SolidConfig.s log.debug("Alias $alias does not start with $ or @, returning it as-is") } val configFile = "tsconfig.json" - val tsConfig = FileManager(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found").also { - log.error("Failed to get $configFile, throwing exception") - } + val tsConfig = FileManager(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found") val aliasPath = Json.parseToJsonElement(tsConfig) .jsonObject["compilerOptions"] ?.jsonObject?.get("paths") ?.jsonObject?.get("${alias.substringBefore("/")}/*") ?.jsonArray?.get(0) - ?.jsonPrimitive?.content ?: throw Exception("Cannot find alias $alias").also { - log.error("Failed to find alias $alias in $tsConfig, throwing exception") - } + ?.jsonPrimitive?.content ?: throw Exception("Cannot find alias $alias in $tsConfig") return aliasPath.replace(Regex("^\\.+/"), "") .replace(Regex("\\*$"), alias.substringAfter("/")).also { log.debug("Resolved alias $alias to $it") diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SvelteSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SvelteSource.kt index f889f0e..15c7053 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SvelteSource.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SvelteSource.kt @@ -30,9 +30,7 @@ class SvelteSource(project: Project) : Source(project, SvelteConfi val fileManager = FileManager(project) var tsConfig = fileManager.getFileContentsAtPath(configFile) if (tsConfig == null) { - if (!usesKit) throw NoSuchFileException("Cannot get $configFile").also { - log.error("Failed to get $configFile, throwing exception") - } + if (!usesKit) throw NoSuchFileException("Cannot get $configFile") val res = ShellRunner(project).execute(arrayOf("npx", "svelte-kit", "sync")) if (res == null) { NotificationManager(project).sendNotification( @@ -44,18 +42,14 @@ class SvelteSource(project: Project) : Source(project, SvelteConfi throw NoSuchFileException("Cannot get or generate $configFile") } Thread.sleep(250) // wait for the sync to create the files - tsConfig = fileManager.getFileContentsAtPath(configFile) ?: throw NoSuchFileException("Cannot get $configFile").also { - log.error("Failed to get $configFile once again, throwing exception") - } + tsConfig = fileManager.getFileContentsAtPath(configFile) ?: throw NoSuchFileException("Cannot get $configFile") } val aliasPath = Json.parseToJsonElement(tsConfig) .jsonObject["compilerOptions"] ?.jsonObject?.get("paths") ?.jsonObject?.get(alias.substringBefore("/")) ?.jsonArray?.get(0) - ?.jsonPrimitive?.content ?: throw Exception("Cannot find alias $alias").also { - log.error("Failed to find alias $alias in $tsConfig, throwing exception") - } + ?.jsonPrimitive?.content ?: throw Exception("Cannot find alias $alias in $tsConfig") return "${aliasPath.replace(Regex("^\\.+/"), "")}/${alias.substringAfter("/")}".also { log.debug("Resolved alias $alias to $it") } diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt index 862b511..938cb83 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt @@ -45,14 +45,10 @@ class VueSource(project: Project) : Source(project, VueConfig.seriali else -> "tsconfig.json" }.let { if (!config.typescript) "jsconfig.json" else it } - val tsConfig = FileManager(project).getFileContentsAtPath(tsConfigLocation) ?: throw NoSuchFileException("$tsConfigLocation not found").also { - log.error("Failed to get $tsConfigLocation, throwing exception") - } + val tsConfig = FileManager(project).getFileContentsAtPath(tsConfigLocation) ?: throw NoSuchFileException("$tsConfigLocation not found") val aliasPath = (resolvePath(tsConfig) ?: if (config.typescript) { resolvePath("tsconfig.app.json") - } else null) ?: throw Exception("Cannot find alias $alias").also { - log.error("Failed to find alias $alias in $tsConfig, throwing exception") - } + } else null) ?: throw Exception("Cannot find alias $alias in $tsConfig") return aliasPath.replace(Regex("^\\.+/"), "") .replace(Regex("\\*$"), alias.substringAfter("/")).also { log.debug("Resolved alias $alias to $it") diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPWindowContents.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPWindowContents.kt index 2d5b98a..8467930 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPWindowContents.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPWindowContents.kt @@ -1,6 +1,7 @@ package com.github.warningimhack3r.intellijshadcnplugin.ui import com.github.warningimhack3r.intellijshadcnplugin.backend.SourceScanner +import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.Source import com.intellij.openapi.application.runReadAction import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.diagnostic.logger @@ -8,9 +9,7 @@ import com.intellij.openapi.project.Project import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBTextField import com.intellij.util.ui.JBUI -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async +import kotlinx.coroutines.* import kotlinx.coroutines.future.asCompletableFuture import java.awt.BorderLayout import java.awt.Color @@ -47,68 +46,78 @@ class ISPWindowContents(private val project: Project) { DISABLE_ROW } - @OptIn(DelicateCoroutinesApi::class) fun panel() = JPanel(GridLayout(0, 1)).apply { border = JBUI.Borders.empty(10) - // Add a component panel - add(createPanel("Add a component") { - GlobalScope.async { - val source = runReadAction { SourceScanner.findShadcnImplementation(project) } - if (source == null) return@async emptyList().also { log.error("No source found for panel 1") } - val installedComponents = runReadAction { source.getInstalledComponents() } - runReadAction { source.fetchAllComponents() }.map { component -> - Item( - component.name, - component.description ?: "${component.name.replace("-", " ") - .replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() - }} component for ${source.framework}", - listOf( - LabeledAction("Add", CompletionAction.DISABLE_ROW) { - runWriteAction { source.addComponent(component.name) } - } - ), - installedComponents.contains(component.name) - ) - }.also { - log.info("Fetched and rendering ${it.size} remote components: ${it.joinToString(", ") { component -> component.title }}") - } - }.asCompletableFuture() - }.apply { - border = BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 0, 1, 0, JBUI.CurrentTheme.ToolWindow.borderColor()), - JBUI.Borders.emptyBottom(10) - ) - }) + val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + var source: Source<*>? = null + var installedComponents = emptyList() + coroutineScope.launch { + source = runReadAction { SourceScanner.findShadcnImplementation(project) } + if (source == null) { + log.error("No shadcn/ui source found") + throw IllegalStateException("No shadcn/ui source found") + } + installedComponents = runReadAction { source!!.getInstalledComponents() } + }.invokeOnCompletion { throwable -> + if (throwable != null && throwable !is CancellationException) { + log.error("Failed to fetch source and installed components", throwable) + return@invokeOnCompletion + } + // Add a component panel + add(createPanel("Add a component") { + coroutineScope.async { + runReadAction { source!!.fetchAllComponents() }.map { component -> + Item( + component.name, + "${component.name.replace("-", " ") + .replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + }} component for ${source!!.framework}", + listOf( + LabeledAction("Add", CompletionAction.DISABLE_ROW) { + runWriteAction { source!!.addComponent(component.name) } + } + ), + installedComponents.contains(component.name) + ) + }.also { + log.info("Fetched and rendering ${it.size} remote components: ${it.joinToString(", ") { component -> component.title }}") + } + }.asCompletableFuture() + }.apply { + border = BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, JBUI.CurrentTheme.ToolWindow.borderColor()), + JBUI.Borders.emptyBottom(10) + ) + }) - // Manage components panel - add(createPanel("Manage components") { - GlobalScope.async { - val source = runReadAction { SourceScanner.findShadcnImplementation(project) } - if (source == null) return@async emptyList().also { log.error("No source found for panel 2") } - runReadAction { source.getInstalledComponents() }.map { component -> - Item( - component, - null, - listOfNotNull( - LabeledAction("Update", CompletionAction.REMOVE_TRIGGER) { - runWriteAction { source.addComponent(component) } - }.takeIf { - runReadAction { !source.isComponentUpToDate(component) } - }, - LabeledAction("Remove", CompletionAction.REMOVE_ROW) { - runWriteAction { source.removeComponent(component) } - } + // Manage components panel + add(createPanel("Manage components") { + coroutineScope.async { + installedComponents.map { component -> + Item( + component, + null, + listOfNotNull( + LabeledAction("Update", CompletionAction.REMOVE_TRIGGER) { + runWriteAction { source!!.addComponent(component) } + }.takeIf { + runReadAction { !source!!.isComponentUpToDate(component) } + }, + LabeledAction("Remove", CompletionAction.REMOVE_ROW) { + runWriteAction { source!!.removeComponent(component) } + } + ) ) - ) - }.also { - log.info("Fetched and rendering ${it.size} installed components: ${it.joinToString(", ") { component -> component.title }}") - } - }.asCompletableFuture() - }.apply { - border = JBUI.Borders.emptyTop(10) - }) + }.also { + log.info("Fetched and rendering ${it.size} installed components: ${it.joinToString(", ") { component -> component.title }}") + } + }.asCompletableFuture() + }.apply { + border = JBUI.Borders.emptyTop(10) + }) + } log.info("Successfully created initial panel") }