diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/SourceScanner.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/SourceScanner.kt index 05df32a..f74296a 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/SourceScanner.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/SourceScanner.kt @@ -13,7 +13,7 @@ object SourceScanner { val log = logger() fun findShadcnImplementation(project: Project): Source<*>? { - val fileManager = FileManager(project) + val fileManager = FileManager.getInstance(project) return fileManager.getFileContentsAtPath("components.json")?.let { componentsJson -> val contents = Json.parseToJsonElement(componentsJson).jsonObject val schema = contents["\$schema"]?.jsonPrimitive?.content ?: "" 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 9492b1e..ade9544 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 @@ -2,19 +2,29 @@ package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers import com.github.warningimhack3r.intellijshadcnplugin.notifications.NotificationManager import com.intellij.notification.NotificationType +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject +@Service(Service.Level.PROJECT) class DependencyManager(private val project: Project) { + companion object { + private val log = logger() + + @JvmStatic + fun getInstance(project: Project): DependencyManager = project.service() + } + enum class InstallationType { DEV, PROD } private fun getPackageManager(): String? { - val fileManager = FileManager(project) + val fileManager = FileManager.getInstance(project) return mapOf( "package-lock.json" to "npm", "pnpm-lock.yaml" to "pnpm", @@ -34,9 +44,8 @@ class DependencyManager(private val project: Project) { 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) { + if (ShellRunner.getInstance(project).execute(command) == null) { NotificationManager(project).sendNotification( "Failed to install dependencies", "Failed to install dependencies: ${dependencyNames.joinToString { ", " }} (${command.joinToString(" ")}). Please install it manually.", @@ -54,9 +63,8 @@ class DependencyManager(private val project: Project) { "remove", *dependencyNames.toTypedArray() ).toTypedArray() - val res = ShellRunner(project).execute(command) // check if the uninstallation was successful - if (res == null) { + if (ShellRunner.getInstance(project).execute(command) == null) { NotificationManager(project).sendNotification( "Failed to uninstall dependencies", "Failed to uninstall dependencies (${command.joinToString(" ")}). Please uninstall them manually.", @@ -68,14 +76,14 @@ class DependencyManager(private val project: Project) { fun getInstalledDependencies(): List { // Read the package.json file - return FileManager(project).getFileContentsAtPath("package.json")?.let { packageJson -> + return FileManager.getInstance(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") + log.debug("Installed dependencies: $it") } } ?: emptyList().also { - logger().error("package.json not found") + log.error("package.json not found") } } 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 8a63813..86b9f42 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 @@ -2,6 +2,8 @@ package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers import com.intellij.openapi.application.runReadAction import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile @@ -11,9 +13,13 @@ import com.intellij.psi.search.GlobalSearchScope import java.io.IOException import java.nio.file.NoSuchFileException +@Service(Service.Level.PROJECT) class FileManager(private val project: Project) { companion object { private val log = logger() + + @JvmStatic + fun getInstance(project: Project): FileManager = project.service() } fun saveFileAtPath(file: PsiFile, path: String) { diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/PsiHelper.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/PsiHelper.kt index d0615b8..6aa682e 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/PsiHelper.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/PsiHelper.kt @@ -8,7 +8,6 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiFileFactory object PsiHelper { - fun createPsiFile(project: Project, fileName: String, text: String): PsiFile { assert(fileName.contains('.')) { "File name must contain an extension" } return runReadAction { 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 03d61fa..a1f4500 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 @@ -1,24 +1,31 @@ package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import java.io.File -class ShellRunner(private val project: Project? = null) { +@Service(Service.Level.PROJECT) +class ShellRunner(private val project: Project) { companion object { private val log = logger() + + @JvmStatic + fun getInstance(project: Project): ShellRunner = project.service() } - private val failedCommands = mutableSetOf() + private val failedWindowsPrograms = mutableSetOf() private fun isWindows() = System.getProperty("os.name").lowercase().contains("win") fun execute(command: Array): String? { - val commandName = command.firstOrNull() ?: return null.also { + val program = command.firstOrNull() ?: return null.also { log.warn("No command name provided") } - if (isWindows() && failedCommands.contains(commandName)) { - command[0] = "$commandName.cmd" + if (isWindows() && failedWindowsPrograms.contains(program)) { + command[0] = "$program.cmd" + log.warn("(Re)trying command with .cmd extension: \"${command.joinToString(" ")}\"") } return try { val platformCommand = if (isWindows()) { @@ -28,21 +35,25 @@ class ShellRunner(private val project: Project? = null) { } + command log.debug("Executing command: \"${platformCommand.joinToString(" ")}\"") val process = ProcessBuilder(*platformCommand) - .redirectOutput(ProcessBuilder.Redirect.PIPE) .directory(project?.basePath?.let { File(it) }) .start() process.waitFor() - process.inputStream.bufferedReader().readText().also { - log.debug("Successfully executed \"${platformCommand.joinToString(" ")}\": $it") + val output = process.inputStream?.bufferedReader()?.readText()?.also { + log.debug("Successfully executed \"${platformCommand.joinToString(" ")}\" with output:\n$it") + } + val error = process.errorStream?.bufferedReader()?.readText() + if (output.isNullOrBlank() && !error.isNullOrBlank()) { + log.warn("Error while executing \"${platformCommand.joinToString(" ")}\":\n${error.take(150)}") } + output } catch (e: Exception) { - if (isWindows() && !commandName.endsWith(".cmd")) { + if (isWindows() && !program.endsWith(".cmd")) { log.warn( - "Failed to execute \"${command.joinToString(" ")}\". Trying to execute \"$commandName.cmd\" instead", + "Failed to execute \"${command.joinToString(" ")}\". Trying to execute \"$program.cmd\" instead", e ) - failedCommands.add(commandName) - return execute(arrayOf("$commandName.cmd") + command.drop(1).toTypedArray()) + failedWindowsPrograms.add(program) + return execute(arrayOf("$program.cmd") + command.drop(1).toTypedArray()) } log.warn("Error while executing \"${command.joinToString(" ")}\"", e) null 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 51640f7..31a1a36 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 @@ -70,7 +70,7 @@ abstract class Source(val project: Project, private val serializer: protected open fun getLocalConfig(): C { return config?.also { log.debug("Returning cached config") - } ?: FileManager(project).getFileContentsAtPath(configFile)?.let { + } ?: FileManager.getInstance(project).getFileContentsAtPath(configFile)?.let { log.debug("Parsing config from $configFile") try { Json.decodeFromString(serializer, it).also { @@ -152,7 +152,7 @@ abstract class Source(val project: Project, private val serializer: } // Public methods - open fun fetchAllComponents(): List { + fun fetchAllComponents(): List { return RequestSender.sendRequest("$domain/registry/index.json").ok { Json.decodeFromString>(it.body) }?.also { @@ -162,8 +162,8 @@ abstract class Source(val project: Project, private val serializer: } } - open fun getInstalledComponents(): List { - return FileManager(project).getFileAtPath( + fun getInstalledComponents(): List { + return FileManager.getInstance(project).getFileAtPath( "${resolveAlias(getLocalPathForComponents())}/ui" )?.children?.map { file -> if (file.isDirectory) file.name else file.name.substringBeforeLast(".") @@ -174,12 +174,12 @@ abstract class Source(val project: Project, private val serializer: } } - open fun addComponent(componentName: String) { + fun addComponent(componentName: String) { val componentsPath = resolveAlias(getLocalPathForComponents()) // Install component val component = fetchComponent(componentName) val installedComponents = getInstalledComponents() - val fileManager = FileManager(project) + val fileManager = FileManager.getInstance(project) val notifManager = NotificationManager(project) log.debug("Installing ${component.name} (installed: ${installedComponents.joinToString(", ")})") setOf(component, *getRegistryDependencies(component).filter { @@ -241,7 +241,7 @@ abstract class Source(val project: Project, private val serializer: } // Install dependencies - val depsManager = DependencyManager(project) + val depsManager = DependencyManager.getInstance(project) val depsToInstall = component.dependencies.filter { dependency -> !depsManager.isDependencyInstalled(dependency) } @@ -275,7 +275,7 @@ abstract class Source(val project: Project, private val serializer: } } - open fun isComponentUpToDate(componentName: String): Boolean { + fun isComponentUpToDate(componentName: String): Boolean { val remoteComponent = fetchComponent(componentName) val componentPath = "${resolveAlias(getLocalPathForComponents())}/${remoteComponent.type.substringAfterLast(":")}${ @@ -283,7 +283,7 @@ abstract class Source(val project: Project, private val serializer: "/${remoteComponent.name}" } else "" }" - val fileManager = FileManager(project) + val fileManager = FileManager.getInstance(project) return remoteComponent.files.all { file -> val psiFile = PsiHelper.createPsiFile( project, adaptFileExtensionToConfig(file.name), file.content @@ -302,10 +302,10 @@ abstract class Source(val project: Project, private val serializer: val componentsDir = "${resolveAlias(getLocalPathForComponents())}/${remoteComponent.type.substringAfterLast(":")}" if (usesDirectoriesForComponents()) { - FileManager(project).deleteFileAtPath("$componentsDir/${remoteComponent.name}") + FileManager.getInstance(project).deleteFileAtPath("$componentsDir/${remoteComponent.name}") } else { remoteComponent.files.forEach { file -> - FileManager(project).deleteFileAtPath("$componentsDir/${file.name}") + FileManager.getInstance(project).deleteFileAtPath("$componentsDir/${file.name}") } } // Remove dependencies no longer needed by any component @@ -314,9 +314,10 @@ abstract class Source(val project: Project, private val serializer: 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 - } + val uselessDependencies = + DependencyManager.getInstance(project).getInstalledDependencies().filter { dependency -> + dependency in allPossiblyNeededDependencies && dependency !in currentlyNeededDependencies + } if (uselessDependencies.isNotEmpty()) { val multipleDependencies = uselessDependencies.size > 1 val notifManager = NotificationManager(project) @@ -331,7 +332,7 @@ abstract class Source(val project: Project, private val serializer: listOf( NotificationAction.createSimple("Remove") { runAsync { - DependencyManager(project).uninstallDependencies(uselessDependencies) + DependencyManager.getInstance(project).uninstallDependencies(uselessDependencies) }.then { notifManager.sendNotificationAndHide( "Removed dependenc${if (multipleDependencies) "ies" else "y"}", 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 b5a53a7..c533550 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 @@ -37,7 +37,7 @@ open class ReactSource(project: Project) : Source(project, ReactCon return alias } val configFile = if (getLocalConfig().tsx) "tsconfig.json" else "jsconfig.json" - val tsConfig = FileManager(project).getFileContentsAtPath(configFile) + val tsConfig = FileManager.getInstance(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found") val aliasPath = parseTsConfig(tsConfig) .jsonObject["compilerOptions"] 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 b0b31ae..e17eb38 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 @@ -42,7 +42,7 @@ open class SolidSource(project: Project) : Source(project, SolidCon return alias } val configFile = "tsconfig.json" - val tsConfig = FileManager(project).getFileContentsAtPath(configFile) + val tsConfig = FileManager.getInstance(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found") val aliasPath = parseTsConfig(tsConfig) .jsonObject["compilerOptions"] diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidUISource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidUISource.kt index a621321..54d64da 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidUISource.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidUISource.kt @@ -37,7 +37,7 @@ open class SolidUISource(project: Project) : Source(project, Soli return alias } val configFile = if (getLocalConfig().tsx) "tsconfig.json" else "jsconfig.json" - val tsConfig = FileManager(project).getFileContentsAtPath(configFile) + val tsConfig = FileManager.getInstance(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found") val aliasPath = parseTsConfig(tsConfig) .jsonObject["compilerOptions"] 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 75b4bb6..f0214ac 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 @@ -37,14 +37,14 @@ open class SvelteSource(project: Project) : Source(project, Svelte log.warn("Alias $alias does not start with $, @ or ~, returning it as-is") return alias } - val usesKit = DependencyManager(project).isDependencyInstalled("@sveltejs/kit") + val usesKit = DependencyManager.getInstance(project).isDependencyInstalled("@sveltejs/kit") val tsConfigName = if (getLocalConfig().typescript) "tsconfig.json" else "jsconfig.json" val configFile = if (usesKit) ".svelte-kit/$tsConfigName" else tsConfigName - val fileManager = FileManager(project) + val fileManager = FileManager.getInstance(project) var tsConfig = fileManager.getFileContentsAtPath(configFile) if (tsConfig == null) { if (!usesKit) throw NoSuchFileException("Cannot get $configFile") - val res = ShellRunner(project).execute(arrayOf("npx", "svelte-kit", "sync")) + val res = ShellRunner.getInstance(project).execute(arrayOf("npx", "svelte-kit", "sync")) if (res == null) { NotificationManager(project).sendNotification( "Failed to generate $configFile", 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 c1a8c55..f485cf8 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 @@ -52,7 +52,7 @@ open class VueSource(project: Project) : Source(project, VueConfig.se else -> "tsconfig.json" }.let { if (!config.typescript) "jsconfig.json" else it } - val tsConfig = FileManager(project).getFileContentsAtPath(tsConfigLocation) + val tsConfig = FileManager.getInstance(project).getFileContentsAtPath(tsConfigLocation) ?: throw NoSuchFileException("$tsConfigLocation not found") val aliasPath = (resolvePath(tsConfig, tsConfigLocation) ?: if (config.typescript) { resolvePath("tsconfig.app.json", "tsconfig.app.json") diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPPanelPopulator.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPPanelPopulator.kt index d5fe784..ca4d55e 100644 --- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPPanelPopulator.kt +++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPPanelPopulator.kt @@ -23,7 +23,7 @@ class ISPPanelPopulator(private val project: Project) { CoroutineScope(SupervisorJob() + Dispatchers.Default).async { return@async Pair( SourceScanner.findShadcnImplementation(project), - FileManager(project).getVirtualFilesByName("package.json").size + FileManager.getInstance(project).getVirtualFilesByName("package.json").size ) }.asCompletableFuture().thenApplyAsync { (source, packageJsonCount) -> log.info("Shadcn implementation detected: $source, package.json count: $packageJsonCount")