diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index ce1c62c..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index bf0c939..481bad5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ plugins { alias(libs.plugins.changelog) // Gradle Changelog Plugin alias(libs.plugins.qodana) // Gradle Qodana Plugin alias(libs.plugins.kover) // Gradle Kover Plugin + kotlin("plugin.serialization") version "1.9.22" } group = properties("pluginGroup").get() @@ -18,12 +19,15 @@ version = properties("pluginVersion").get() // Configure project's dependencies repositories { + mavenLocal() mavenCentral() } // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog dependencies { -// implementation(libs.annotations) + implementation(files("libs/espimg-0.1.0.jar")) + implementation("org.java-websocket:Java-WebSocket:1.5.4") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") } // Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+. @@ -73,6 +77,24 @@ tasks { gradleVersion = properties("gradleVersion").get() } + runIde { + autoReloadPlugins.set(true) + } + + // to allow hot reload of sandbox using buildPlugin + buildSearchableOptions { + enabled = false + } + + // Set the JVM compatibility versions + withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + withType { + kotlinOptions.jvmTarget = "17" + } + patchPluginXml { version = properties("pluginVersion") sinceBuild = properties("pluginSinceBuild") @@ -83,7 +105,7 @@ tasks { val start = "" val end = "" - with (it.lines()) { + with(it.lines()) { if (!containsAll(listOf(start, end))) { throw GradleException("Plugin description section not found in README.md:\n$start ... $end") } @@ -126,6 +148,7 @@ tasks { // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel - channels = properties("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) } + channels = + properties("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) } } } diff --git a/gradle.properties b/gradle.properties index 90ba70c..49a8a62 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ pluginUntilBuild = 233.* # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension platformType = IC -platformVersion = 2022.3.3 +platformVersion = 2023.3.2 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 diff --git a/libs/espimg-0.1.0.jar b/libs/espimg-0.1.0.jar new file mode 100644 index 0000000..f9eb525 Binary files /dev/null and b/libs/espimg-0.1.0.jar differ diff --git a/settings.gradle.kts b/settings.gradle.kts index 29763db..d597ebc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ + plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" } diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt new file mode 100644 index 0000000..be49a63 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiRestartAction.kt @@ -0,0 +1,14 @@ +package com.github.jozott00.wokwiintellij.actions + +import com.github.jozott00.wokwiintellij.services.WokwiProjectService +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.ToggleAction +import com.intellij.openapi.components.service + +class WokwiRestartAction : AnAction() { + override fun actionPerformed(p0: AnActionEvent) { + p0.project?.service()?.restartSimulation() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStartAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStartAction.kt new file mode 100644 index 0000000..4e33042 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStartAction.kt @@ -0,0 +1,16 @@ +package com.github.jozott00.wokwiintellij.actions + + +import com.github.jozott00.wokwiintellij.services.WokwiComponentService +import com.github.jozott00.wokwiintellij.services.WokwiProjectService +import com.github.jozott00.wokwiintellij.services.WokwiSimulationService +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service + +class WokwiStartAction : AnAction() { + override fun actionPerformed(event: AnActionEvent) { + val s = event.project?.service() + s?.startSimulator() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt new file mode 100644 index 0000000..4cf7d7f --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiStopAction.kt @@ -0,0 +1,18 @@ +package com.github.jozott00.wokwiintellij.actions + + +import com.github.jozott00.wokwiintellij.services.WokwiComponentService +import com.github.jozott00.wokwiintellij.services.WokwiProjectService +import com.github.jozott00.wokwiintellij.services.WokwiSimulationService +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service +import com.intellij.openapi.ui.Messages + +class WokwiStopAction : AnAction() { + override fun actionPerformed(event: AnActionEvent) { + val s = event.project?.service() + s?.stopSimulator() + + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt new file mode 100644 index 0000000..46fca37 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/actions/WokwiWatchAction.kt @@ -0,0 +1,24 @@ +package com.github.jozott00.wokwiintellij.actions + +import com.github.jozott00.wokwiintellij.services.WokwiProjectService +import com.github.jozott00.wokwiintellij.states.WokwiConfigState +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.ToggleAction +import com.intellij.openapi.components.service + +class WokwiWatchAction : ToggleAction() { + + override fun isSelected(p0: AnActionEvent): Boolean { + return p0.project?.service()?.state?.watchElf ?: false + } + + override fun setSelected(even: AnActionEvent, watchEnabled: Boolean) { + even.project?.service()?.state?.watchElf = watchEnabled + if (watchEnabled) { + even.project?.service()?.watchStart() + } else { + even.project?.service()?.watchStop() + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/MyApplicationActivationListener.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt similarity index 84% rename from src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/MyApplicationActivationListener.kt rename to src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt index 666a6cc..77ff3b5 100644 --- a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/MyApplicationActivationListener.kt +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiActivationListener.kt @@ -4,7 +4,7 @@ import com.intellij.openapi.application.ApplicationActivationListener import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.wm.IdeFrame -internal class MyApplicationActivationListener : ApplicationActivationListener { +internal class WokwiActivationListener : ApplicationActivationListener { override fun applicationActivated(ideFrame: IdeFrame) { thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.") diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiElfFileListener.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiElfFileListener.kt new file mode 100644 index 0000000..9fdbd0b --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiElfFileListener.kt @@ -0,0 +1,38 @@ +package com.github.jozott00.wokwiintellij.listeners + +import com.github.jozott00.wokwiintellij.services.WokwiProjectService +import com.github.jozott00.wokwiintellij.states.WokwiConfigState +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.BulkFileListener +import com.intellij.openapi.vfs.newvfs.events.VFileEvent + +class WokwiElfFileListener(val project: Project) : BulkFileListener { + + + override fun after(events: MutableList) { + val configState = project.service() + + if (!configState.watchElf) return + + val watchPath = configState.elfPath + + val result = events.find { + if (it.file?.isInLocalFileSystem != true) + return@find false + + if (it.file?.path == watchPath) + return@find true + + false + } + + val projectService = project.service() + if (result != null) { + projectService.elfFileUpdate() + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiPostStartupActivity.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiPostStartupActivity.kt new file mode 100644 index 0000000..4f0cc6c --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiPostStartupActivity.kt @@ -0,0 +1,14 @@ +package com.github.jozott00.wokwiintellij.listeners + +import com.github.jozott00.wokwiintellij.services.WokwiProjectService +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.startup.StartupActivity + +class WokwiPostStartupActivity : ProjectActivity { + override suspend fun execute(project: Project) { + val projectService = project.service() + projectService.startup() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiProjectManagerListener.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiProjectManagerListener.kt new file mode 100644 index 0000000..e49020a --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/listeners/WokwiProjectManagerListener.kt @@ -0,0 +1,11 @@ +package com.github.jozott00.wokwiintellij.listeners + +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.github.jozott00.wokwiintellij.MyBundle +import com.intellij.openapi.project.ProjectManagerListener +import com.intellij.openapi.project.impl.ProjectLifecycleListener + + + diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/MyProjectService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/MyProjectService.kt deleted file mode 100644 index 279c8c2..0000000 --- a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/MyProjectService.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.jozott00.wokwiintellij.services - -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.project.Project -import com.github.jozott00.wokwiintellij.MyBundle - -@Service(Service.Level.PROJECT) -class MyProjectService(project: Project) { - - init { - thisLogger().info(MyBundle.message("projectService", project.name)) - thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.") - } - - fun getRandomNumber() = (1..100).random() -} diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt new file mode 100644 index 0000000..da8fc50 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiComponentService.kt @@ -0,0 +1,27 @@ +package com.github.jozott00.wokwiintellij.services + +import com.github.jozott00.wokwiintellij.states.WokwiConfigState +import com.github.jozott00.wokwiintellij.toolWindow.SimulatorPanel +import com.github.jozott00.wokwiintellij.toolWindow.WokwiToolWindow +import com.github.jozott00.wokwiintellij.toolWindow.wokwiConfigPanel +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import javax.swing.JPanel + + +@Service(Service.Level.PROJECT) +class WokwiComponentService(val project: Project) { + + val wokwiConfigState = project.service() + + val simulatorPanel = SimulatorPanel() + val configPanel = wokwiConfigPanel(wokwiConfigState.state) { + onChangeAction = { + println("Changes in model: ${wokwiConfigState.state}") + } + } + + val toolWindow = WokwiToolWindow(configPanel, simulatorPanel) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiDataService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiDataService.kt new file mode 100644 index 0000000..419b5bc --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiDataService.kt @@ -0,0 +1,95 @@ +package com.github.jozott00.wokwiintellij.services + +import com.github.jozott00.wokwiintellij.states.WokwiConfigState +import com.github.jozott00.wokwiintellij.utils.WokwiNotifier +import com.intellij.notification.NotificationType +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VfsUtil +import espimg.EspImg +import espimg.ImageResult +import espimg.exceptions.EspImgException +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.attribute.BasicFileAttributes + +@Service(Service.Level.PROJECT) +class WokwiDataService(val project: Project) { + + private val configState = project.service() + + private var lastFile: String? = null + private var lastModifiedTime: Long? = null + private var image: ImageResult? = null + + fun retrieveImage(): ImageResult? { + if (checkForReload()) + loadImage() + + return image + } + + private fun checkForReload(): Boolean { + if (configState.elfPath != lastFile) { + return true + } + + val path = configState.elfPath + try { + val currentTimeStamp = this.readFileModification(path) + + if (configState.elfPath != lastFile || currentTimeStamp != lastModifiedTime) { + println("RELOAD REQUIRED: ${configState.elfPath} vs $lastFile ... $currentTimeStamp vs $lastModifiedTime") + return true + } + } catch (e: IOException) { + println("RELOAD REQUIRED: EXCETPION $e") + return true + } + + + return false + } + + private fun loadImage(): Boolean { + val path = configState.elfPath + println("LOADING IMAGE $path") + val file = File(path) + + if (!file.exists()) { + thisLogger().warn("File $file does not exist!") + } + + val vfile = VfsUtil.findFileByIoFile(file, true) + + if (vfile == null) { + WokwiNotifier.notifyBalloon("ELF file `$path` not found", project, NotificationType.ERROR) + return false; + } + + val inputStream: InputStream = vfile.inputStream + + try { + this.image = EspImg.getFlashImage(inputStream.readAllBytes(), null, null) + this.lastModifiedTime = this.readFileModification(path) + } catch (e: EspImgException) { + WokwiNotifier.notifyBalloon("${e.message}", project, NotificationType.ERROR) + return false; + } + + lastFile = file.path + return true + } + + private fun readFileModification(path: String): Long { + val fileAttributes = Files.readAttributes(Paths.get(path), BasicFileAttributes::class.java) + return fileAttributes.lastModifiedTime().toMillis() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt new file mode 100644 index 0000000..2fc3145 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiProjectService.kt @@ -0,0 +1,76 @@ +package com.github.jozott00.wokwiintellij.services + +import com.github.jozott00.wokwiintellij.listeners.WokwiElfFileListener +import com.github.jozott00.wokwiintellij.states.WokwiConfigState +import com.github.jozott00.wokwiintellij.wokwiServer.WokwiServer +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.util.messages.MessageBusConnection + +@Service(Service.Level.PROJECT) +class WokwiProjectService(val project: Project) : Disposable { + private var server: WokwiServer? = null + + private val componentService = project.service() + private val configState = project.service() + private val dataService = project.service() + private val simulationService = project.service() + + private var msgBusConnection: MessageBusConnection? = null + private var simulationRunning = false; + fun startSimulator() { + if (dataService.retrieveImage() == null) { + return + } + + componentService.toolWindow.showSimulation() + simulationRunning = true + watchStart() + } + + fun stopSimulator() { + componentService.toolWindow.showConfig() + simulationRunning = false + watchStop() + } + + fun startup() { + val port = 9012 // Specify your port here + server = WokwiServer(port, project).apply { + start() + println("WokwiServer started on port: $port") + } + } + + override fun dispose() { + server?.stop() + } + + fun restartSimulation() { + simulationService.restartAll() + } + + fun elfFileUpdate() { + println("FILE UPDATED ... restart") + simulationService.restartAll() + } + + fun watchStart() { + if (!configState.watchElf || !simulationRunning) return + println("START WATCHING") + msgBusConnection = project.messageBus.connect() + msgBusConnection?.subscribe(VirtualFileManager.VFS_CHANGES, WokwiElfFileListener(project)) + } + + fun watchStop() { + println("STOP WATCHING") + msgBusConnection?.disconnect() + msgBusConnection = null + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiSimulationService.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiSimulationService.kt new file mode 100644 index 0000000..31517bb --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/services/WokwiSimulationService.kt @@ -0,0 +1,59 @@ +package com.github.jozott00.wokwiintellij.services + +import com.github.jozott00.wokwiintellij.utils.WokwiNotifier +import com.github.jozott00.wokwiintellij.wokwiServer.WokwiCommand +import com.intellij.notification.NotificationType +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.jetbrains.rd.generator.nova.PredefinedType +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import org.java_websocket.WebSocket + +@Service(Service.Level.PROJECT) +class WokwiSimulationService(val project: Project) { + var connection: WebSocket? = null + + val dataService = project.service() + + + fun messageReceived(msg: Map, conn: WebSocket) { + + } + + fun connect(webSocket: WebSocket): Boolean { + if (this.connection != null) { + return false + } + + this.connection = webSocket + sendStart(webSocket) + + return true + } + + fun restartAll() { + if (connection != null) { + sendStart(connection!!) + } + } + + private fun sendStart(webSocket: WebSocket) { + val image = dataService.retrieveImage() + if (image == null) { + WokwiNotifier.notifyBalloon("Failed to retrieve image", project, NotificationType.ERROR) + return + } + + val cmd = WokwiCommand.start(image.elf, image.romSegments.toList()) + + val json = Json.encodeToString(WokwiCommand.serializer(), cmd) + webSocket.send(json) + } + + fun disconnect(webSocket: WebSocket) { + this.connection = null + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiConfigState.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiConfigState.kt new file mode 100644 index 0000000..477b816 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/states/WokwiConfigState.kt @@ -0,0 +1,45 @@ +package com.github.jozott00.wokwiintellij.states + +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.util.xmlb.XmlSerializerUtil + +@Service(Service.Level.PROJECT) +@State(name = "WokwiConfigModel") +data class WokwiConfigState( + var espDevice: ESPDevice = ESPDevice.ESP32, + var flashSize: FlashSize = FlashSize._4MB, + var elfPath: String = "", + var watchElf: Boolean = true, +) : PersistentStateComponent { + + override fun getState(): WokwiConfigState { + return this + } + + override fun loadState(state: WokwiConfigState) { + XmlSerializerUtil.copyBean(state, this) + } +} + +enum class ESPDevice { + ESP32, + ESP32s2, + ESP32s3, + ESP32c3, + ESP32c6; +} + +enum class FlashSize { + _2MB, + _4MB, + _8MB, + _16MB, + _32MB; + + override fun toString(): String { + return name.removePrefix("_").removeSuffix("MB") + " MB" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/JCEFContent: JPanel().kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/JCEFContent: JPanel().kt new file mode 100644 index 0000000..1a716a0 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/JCEFContent: JPanel().kt @@ -0,0 +1,63 @@ +package com.github.jozott00.wokwiintellij.toolWindow + +import com.intellij.openapi.Disposable +import com.intellij.ui.jcef.JBCefBrowser +import com.intellij.ui.jcef.JBCefBrowserBase +import com.intellij.ui.jcef.JBCefClient.Properties.JS_QUERY_POOL_SIZE +import com.intellij.ui.jcef.JBCefJSQuery +import com.intellij.ui.jcef.JCEFHtmlPanel +import org.cef.browser.CefBrowser +import org.cef.browser.CefFrame +import org.cef.handler.CefLoadHandlerAdapter +import java.awt.BorderLayout +import javax.swing.JPanel + + +class JCEFContent(onLoaded: (JCEFContent) -> Unit) : JPanel(), Disposable { + + val browser: JBCefBrowser = + JCEFHtmlPanel("https://wokwi.com/_alpha/wembed/345144250522927698?partner=espressif&port=9012&data=demo") + + init { + browser.let { + layout = BorderLayout() + add(it.component, BorderLayout.CENTER) + } + } + + init { + browser.jbCefClient.setProperty(JS_QUERY_POOL_SIZE, 5) + browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { + override fun onLoadEnd(cefBrowser: CefBrowser?, frame: CefFrame?, httpStatusCode: Int) { + println("-------------- LOAD END") + + val loadedCallback = JBCefJSQuery.create(browser as JBCefBrowserBase) + loadedCallback.addHandler { _ -> + onLoaded(this@JCEFContent) + null + } + + cefBrowser!!.executeJavaScript( + """ + document.getElementsByTagName("header")[0].style.display = "none"; + document.body.style.overflow = "hidden"; + + var parentDiv = document.querySelector('.simulation_simulationControls__Jqtsp'); + var childDivs = parentDiv.children; + + // Sleep for a little to delay simulator show up + setTimeout(function(){ + ${loadedCallback.inject(null)} + }, 300); + + """.trimIndent(), null, 0 + ) + + } + }, browser.cefBrowser) + } + + override fun dispose() { + browser.dispose() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/MyToolWindowFactory.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/MyToolWindowFactory.kt deleted file mode 100644 index 718c024..0000000 --- a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/MyToolWindowFactory.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.github.jozott00.wokwiintellij.toolWindow - -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.project.Project -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowFactory -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBPanel -import com.intellij.ui.content.ContentFactory -import com.github.jozott00.wokwiintellij.MyBundle -import com.github.jozott00.wokwiintellij.services.MyProjectService -import javax.swing.JButton - - -class MyToolWindowFactory : ToolWindowFactory { - - init { - thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.") - } - - override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val myToolWindow = MyToolWindow(toolWindow) - val content = ContentFactory.getInstance().createContent(myToolWindow.getContent(), null, false) - toolWindow.contentManager.addContent(content) - } - - override fun shouldBeAvailable(project: Project) = true - - class MyToolWindow(toolWindow: ToolWindow) { - - private val service = toolWindow.project.service() - - fun getContent() = JBPanel>().apply { - val label = JBLabel(MyBundle.message("randomLabel", "?")) - - add(label) - add(JButton(MyBundle.message("shuffle")).apply { - addActionListener { - label.text = MyBundle.message("randomLabel", service.getRandomNumber()) - } - }) - } - } -} diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt new file mode 100644 index 0000000..0cbb88c --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorPanel.kt @@ -0,0 +1,100 @@ +package com.github.jozott00.wokwiintellij.toolWindow + + +import com.intellij.ide.wizard.withVisualPadding +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.ui.components.Label +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.LabelPosition +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.util.preferredWidth +import java.awt.BorderLayout +import java.awt.CardLayout +import java.awt.FlowLayout +import javax.swing.JPanel +import javax.swing.JProgressBar + +class SimulatorPanel : JPanel() { + + private val cardLayout = CardLayout() + private var browser: JCEFContent? = null + + private val loadingPanel = panel { + panel { + row { + cell(JProgressBar().apply { + isIndeterminate = true + preferredWidth = 300 + }) + .label("Starting simulator...", LabelPosition.TOP) + + } + }.align(Align.CENTER) + + }.withVisualPadding() + + + init { + layout = cardLayout + add(loadingPanel) + add("LOADING", loadingPanel) + cardLayout.show(this, "LOADING") + } + + + fun loadSimulator() { + browser = JCEFContent { browser -> + cardLayout.show(this, "SIMULATOR") + revalidate() + repaint() + } + + val simulator = simulator(toolbar(), browser!!) + add("SIMULATOR", simulator) + revalidate() + repaint() + } + + fun stopSimulator() { + remove(browser) + browser?.dispose() + + cardLayout.show(this, "LOADING") + + revalidate() + repaint() + } + + private fun simulator(toolbar: JPanel, browser: JPanel): JPanel { + val simulator = JPanel(BorderLayout()) + simulator.add(toolbar, BorderLayout.NORTH) + simulator.add(browser!!, BorderLayout.CENTER) + + return simulator + } + + private fun toolbar(): JPanel { + + val panel = JPanel(BorderLayout()) + + val am = ActionManager.getInstance() + val group = DefaultActionGroup( + am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiStopAction"), + am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiRestartAction"), + am.getAction("com.github.jozott00.wokwiintellij.actions.WokwiWatchAction"), + ) + val toolbar = am.createActionToolbar("com.github.jozott00.wokwiintellij.actions.WokwiToolbar", group, false) + // horizonal orientation + toolbar.orientation = 0 + toolbar.targetComponent = panel + panel.add(toolbar.component, BorderLayout.NORTH) + + + return panel + } + + private fun updateLayout() { + layout = CardLayout() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorWindowFactory.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorWindowFactory.kt new file mode 100644 index 0000000..f614881 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/SimulatorWindowFactory.kt @@ -0,0 +1,34 @@ +package com.github.jozott00.wokwiintellij.toolWindow + +import com.github.jozott00.wokwiintellij.actions.WokwiRestartAction +import com.github.jozott00.wokwiintellij.services.WokwiComponentService +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.ui.content.ContentFactory +import com.intellij.ui.dsl.builder.panel +import org.jdesktop.swingx.action.ActionManager +import javax.swing.JPanel + +class SimulatorWindowFactory : ToolWindowFactory { + + init { + thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.") + } + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val componentService = toolWindow.project.service() + + val toolWindowContent = componentService.toolWindow.getContent() + val content = ContentFactory.getInstance() + .createContent(toolWindowContent, null, false) + toolWindow.contentManager.addContent(content) + + } + + override fun shouldBeAvailable(project: Project) = true + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt new file mode 100644 index 0000000..894de0a --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiConfigPanel.kt @@ -0,0 +1,84 @@ +package com.github.jozott00.wokwiintellij.toolWindow + +import com.github.jozott00.wokwiintellij.states.ESPDevice +import com.github.jozott00.wokwiintellij.states.FlashSize +import com.github.jozott00.wokwiintellij.states.WokwiConfigState +import com.intellij.icons.AllIcons +import com.intellij.ide.wizard.withVisualPadding +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.dsl.builder.* +import com.intellij.ui.dsl.gridLayout.HorizontalAlign +import com.intellij.ui.util.preferredWidth +import java.awt.Font +import javax.swing.* + + +class WokwiConfigPanelBuilder(val model: WokwiConfigState) { + + var onChangeAction: (() -> Unit)? = null + + fun build(): DialogPanel { + var panel: DialogPanel? = null + val action = ActionManager.getInstance().getAction("com.github.jozott00.wokwiintellij.actions.WokwiStartAction") + + fun onChange() { + invokeLater { + if (panel == null) + return@invokeLater + + panel!!.apply() + onChangeAction?.invoke() + } + } + + panel = panel { + lateinit var textField: Cell + + row { + button("Start Simulator", action) + .align(Align.CENTER) + .apply { + this.component.icon = AllIcons.Debugger.ThreadRunning + } + } + + group("Simulation Settings") { + row("ESP target device:") { + comboBox(ESPDevice.entries) + .onChanged { _ -> onChange() } + .bindItem(model::espDevice.toNullableProperty()) + }.rowComment("Wokwi simulator diagram is chosen based on target device.") + + row("Executable: ") { + textField = textFieldWithBrowseButton().apply { + component.preferredWidth = 300 + } + .onChanged { _ -> onChange() } + .bindText(model::elfPath) + }.rowComment("Path to ELF binary to run in simulator") + + row("Flash Size: ") { + comboBox(FlashSize.entries) + .onChanged { _ -> onChange() } + .bindItem(model::flashSize.toNullableProperty()) + }.rowComment("Must be compatible with device and partition table") + } + } + .withVisualPadding() + + + return panel + } + +} + +fun wokwiConfigPanel(model: WokwiConfigState, build: WokwiConfigPanelBuilder.() -> Unit): DialogPanel { + return WokwiConfigPanelBuilder(model).apply(build).build() +} + +private fun JComponent.bold(isBold: Boolean) { + font = font.deriveFont(if (isBold) Font.BOLD else Font.PLAIN) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiToolWindow.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiToolWindow.kt new file mode 100644 index 0000000..fcf83a3 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/toolWindow/WokwiToolWindow.kt @@ -0,0 +1,33 @@ +package com.github.jozott00.wokwiintellij.toolWindow + +import com.intellij.openapi.ui.DialogPanel +import java.awt.BorderLayout +import javax.swing.JPanel + +class WokwiToolWindow(private val configPanel: DialogPanel, private val simulationPanel: SimulatorPanel) { + + private val panel = JPanel(BorderLayout()).apply { + this.add(configPanel) + + } + + fun getContent() = panel + + fun showSimulation() { + if (panel.components.contains(simulationPanel)) return + panel.removeAll() + panel.add(simulationPanel) + simulationPanel.loadSimulator() + panel.revalidate() + } + + fun showConfig() { + if (panel.components.contains(configPanel)) return + panel.removeAll() + panel.add(configPanel) + simulationPanel.stopSimulator() + panel.revalidate() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiNotifier.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiNotifier.kt new file mode 100644 index 0000000..49fa244 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/utils/WokwiNotifier.kt @@ -0,0 +1,19 @@ +package com.github.jozott00.wokwiintellij.utils + +import com.intellij.notification.NotificationGroup +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.project.Project + +object WokwiNotifier { + + private val NOTIFICATION_GROUP = "Wokwi Simulator" + + fun notifyBalloon(message: String, project: Project, type: NotificationType = NotificationType.INFORMATION) { + NotificationGroupManager.getInstance() + .getNotificationGroup(NOTIFICATION_GROUP) + .createNotification(message, type) + .notify(project) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiCommand.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiCommand.kt new file mode 100644 index 0000000..b015ca9 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiCommand.kt @@ -0,0 +1,46 @@ +package com.github.jozott00.wokwiintellij.wokwiServer + +import espimg.RomSegment +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import java.util.* + + +@Serializable +data class WokwiCommand( + val type: String, + val elf: String, + val espBin: List> +) { + companion object { + fun start(bytes: ByteArray, segments: List): WokwiCommand { + val bootloader = segments[0] + val partitionTable = segments[1] + val app = segments[2] + + return WokwiCommand( + type = "start", + elf = bytes.toBase64(), + espBin = listOf( + romSegToVec(bootloader), + romSegToVec(partitionTable), + romSegToVec(app), + ) + ) + } + + private fun romSegToVec(romSeg: RomSegment): List { + return listOf( + JsonPrimitive(romSeg.addr), + JsonPrimitive(romSeg.data.toBase64()), + ) + } + } +} + +private fun ByteArray.toBase64(): String = + String(Base64.getEncoder().encode(this)) + + + diff --git a/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiServer.kt b/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiServer.kt new file mode 100644 index 0000000..674b8f7 --- /dev/null +++ b/src/main/kotlin/com/github/jozott00/wokwiintellij/wokwiServer/WokwiServer.kt @@ -0,0 +1,43 @@ +package com.github.jozott00.wokwiintellij.wokwiServer + +import com.github.jozott00.wokwiintellij.services.WokwiProjectService +import com.github.jozott00.wokwiintellij.services.WokwiSimulationService +import com.intellij.openapi.components.service +import com.intellij.openapi.components.services +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import org.java_websocket.WebSocket +import org.java_websocket.handshake.ClientHandshake +import org.java_websocket.server.WebSocketServer +import java.net.InetSocketAddress + + +// WokwiServer class +class WokwiServer(port: Int, project: Project) : WebSocketServer(InetSocketAddress(port)) { + + val service = project.service() + + override fun onOpen(conn: WebSocket, handshake: ClientHandshake) { + service.connect(conn) + } + + override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) { + service.disconnect(conn) + } + + override fun onMessage(conn: WebSocket, message: String) { + val data = Json.parseToJsonElement(message) + require(data is JsonObject) { "Failed to parse received message $message" } + this.service.messageReceived(data, conn) + } + + override fun onStart() { + + } + + override fun onError(conn: WebSocket?, ex: Exception) { + ex.printStackTrace() + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d65904d..e7ac195 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -9,10 +9,60 @@ messages.MyBundle - + + + + + + - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/kotlin/com/github/jozott00/wokwiintellij/MyPluginTest.kt b/src/test/kotlin/com/github/jozott00/wokwiintellij/MyPluginTest.kt index 086c2c4..93b4109 100644 --- a/src/test/kotlin/com/github/jozott00/wokwiintellij/MyPluginTest.kt +++ b/src/test/kotlin/com/github/jozott00/wokwiintellij/MyPluginTest.kt @@ -6,7 +6,6 @@ import com.intellij.psi.xml.XmlFile import com.intellij.testFramework.TestDataPath import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.PsiErrorElementUtil -import com.github.jozott00.wokwiintellij.services.MyProjectService @TestDataPath("\$CONTENT_ROOT/src/test/testData") class MyPluginTest : BasePlatformTestCase() { @@ -29,11 +28,6 @@ class MyPluginTest : BasePlatformTestCase() { myFixture.testRename("foo.xml", "foo_after.xml", "a2") } - fun testProjectService() { - val projectService = project.service() - - assertNotSame(projectService.getRandomNumber(), projectService.getRandomNumber()) - } override fun getTestDataPath() = "src/test/testData/rename" }