diff --git a/src/main/kotlin/com/jetbrains/micropython/actions/MicroPythonAction.kt b/src/main/kotlin/com/jetbrains/micropython/actions/MicroPythonAction.kt index 3a1f226..ecfecce 100644 --- a/src/main/kotlin/com/jetbrains/micropython/actions/MicroPythonAction.kt +++ b/src/main/kotlin/com/jetbrains/micropython/actions/MicroPythonAction.kt @@ -10,10 +10,9 @@ abstract class MicroPythonAction : AnAction() { val project = e.project ?: return val facet = project.firstMicroPythonFacet if (facet != null) { - e.presentation.isEnabled = facet.checkValid() == ValidationResult.OK + e.presentation.isEnabledAndVisible = facet.checkValid() == ValidationResult.OK } else { - e.presentation.isVisible = false - e.presentation.isEnabled = false + e.presentation.isEnabledAndVisible = false } } } diff --git a/src/main/kotlin/com/jetbrains/micropython/actions/RunMicroReplAction.kt b/src/main/kotlin/com/jetbrains/micropython/actions/RunMicroReplAction.kt index 6250caa..b296171 100644 --- a/src/main/kotlin/com/jetbrains/micropython/actions/RunMicroReplAction.kt +++ b/src/main/kotlin/com/jetbrains/micropython/actions/RunMicroReplAction.kt @@ -16,26 +16,27 @@ package com.jetbrains.micropython.actions +import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.module.Module -import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.wm.ToolWindowManager import com.jetbrains.micropython.repl.MicroPythonReplManager import com.jetbrains.micropython.settings.firstMicroPythonFacet class RunMicroReplAction : MicroPythonAction() { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT + override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return val editor = FileEditorManagerEx.getInstanceEx(project).selectedTextEditor ?: return /* - Here we make best effort to find out module which is relevant to the current event. + Here we make our best to find out module which is relevant to the current event. There are two cases to consider: 1) There is an open file present. 2) No files are opened. @@ -48,9 +49,6 @@ class RunMicroReplAction : MicroPythonAction() { project.firstMicroPythonFacet?.module } - if (module != null) { - MicroPythonReplManager.getInstance(module).startREPL() - ToolWindowManager.getInstance(project).getToolWindow("MicroPython")?.show() - } + project.service().startOrRestartRepl() } } diff --git a/src/main/kotlin/com/jetbrains/micropython/repl/MicroPythonReplManager.kt b/src/main/kotlin/com/jetbrains/micropython/repl/MicroPythonReplManager.kt index 8309bd0..07627d3 100644 --- a/src/main/kotlin/com/jetbrains/micropython/repl/MicroPythonReplManager.kt +++ b/src/main/kotlin/com/jetbrains/micropython/repl/MicroPythonReplManager.kt @@ -1,112 +1,24 @@ package com.jetbrains.micropython.repl import com.intellij.openapi.components.Service -import com.intellij.openapi.module.Module -import com.jediterm.terminal.TtyConnector -import com.jetbrains.micropython.settings.MicroPythonFacet -import com.jetbrains.micropython.settings.microPythonFacet -import com.pty4j.PtyProcess -import org.jetbrains.plugins.terminal.LocalTerminalDirectRunner -import org.jetbrains.plugins.terminal.ShellStartupOptions +import com.intellij.openapi.project.Project +import com.intellij.util.messages.Topic -interface CommsEventListener { - fun onProcessStarted(ttyConnector: TtyConnector) - fun onProcessDestroyed() - fun onProcessCreationFailed(reason: String) -} - -@Service -class MicroPythonReplManager(module: Module) { - private val currentModule: Module = module - private var listeners: MutableList = mutableListOf() - private var currentProcess: Process? = null - private var currentConnector: TtyConnector? = null - - companion object { - fun getInstance(module: Module): MicroPythonReplManager = - module.getService(MicroPythonReplManager::class.java) - } - - fun startREPL() { - if (isRunning) { - stopREPL() - } - - val facet = currentModule.microPythonFacet ?: return - val devicePath = facet.getOrDetectDevicePathSynchronously() - - if (facet.pythonPath == null) { - notifyProcessCreationFailed("Valid Python interpreter is needed to start a REPL!") - return - } - - if (devicePath == null) { - notifyProcessCreationFailed("Device path is not specified, please check settings.") - return - } - - val initialShellCommand = mutableListOf( - facet.pythonPath!!, - "${MicroPythonFacet.scriptsPath}/microrepl.py", - devicePath - ) - val terminalRunner = object : LocalTerminalDirectRunner(currentModule.project) { - override fun getInitialCommand(envs: MutableMap): MutableList { - return initialShellCommand - } - - fun getTtyConnector(process: PtyProcess): TtyConnector { - return this.createTtyConnector(process) - } - } - - synchronized(this) { - val terminalOptions = ShellStartupOptions.Builder() - .workingDirectory(devicePath) - .shellCommand(initialShellCommand) - .build() - val process = terminalRunner.createProcess(terminalOptions) - val ttyConnector = terminalRunner.getTtyConnector(process) - - currentProcess = process - currentConnector = ttyConnector - notifyProcessStarted(ttyConnector) - } - } - - fun stopREPL() { - synchronized(this) { - currentProcess?.let { - it.destroy() - notifyProcessDestroyed() - } - currentProcess = null - } - } - - val isRunning: Boolean - get() = currentProcess?.isAlive ?: false - - fun addListener(listener: CommsEventListener) { - listeners.add(listener) +interface MicroPythonReplControl { + fun stopRepl() + fun startOrRestartRepl() +} - currentConnector?.let { listener.onProcessStarted(it) } - } +@Service(Service.Level.PROJECT) +class MicroPythonReplManager(private val project: Project) : MicroPythonReplControl { + override fun stopRepl() = + project.messageBus.syncPublisher(MICROPYTHON_REPL_CONTROL).stopRepl() - fun removeListener(listener: CommsEventListener) { - listeners.remove(listener) - } - private fun notifyProcessStarted(connector: TtyConnector) { - listeners.forEach { it.onProcessStarted(connector) } - } + override fun startOrRestartRepl() = + project.messageBus.syncPublisher(MICROPYTHON_REPL_CONTROL).startOrRestartRepl() - private fun notifyProcessDestroyed() { - listeners.forEach { it.onProcessDestroyed() } - } +} - private fun notifyProcessCreationFailed(reason: String) { - listeners.forEach { it.onProcessCreationFailed(reason) } - } -} \ No newline at end of file +val MICROPYTHON_REPL_CONTROL = Topic(MicroPythonReplControl::class.java) \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/micropython/repl/StopReplBeforeRunTask.kt b/src/main/kotlin/com/jetbrains/micropython/repl/StopReplBeforeRunTask.kt index 650f570..9939854 100644 --- a/src/main/kotlin/com/jetbrains/micropython/repl/StopReplBeforeRunTask.kt +++ b/src/main/kotlin/com/jetbrains/micropython/repl/StopReplBeforeRunTask.kt @@ -5,9 +5,8 @@ import com.intellij.execution.BeforeRunTaskProvider import com.intellij.execution.configurations.RunConfiguration import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.module.Module +import com.intellij.openapi.components.service import com.intellij.openapi.util.Key import com.jetbrains.micropython.run.MicroPythonRunConfiguration import com.jetbrains.micropython.settings.MicroPythonFacetType @@ -38,21 +37,14 @@ class StopReplBeforeRunTaskProvider : BeforeRunTaskProvider().stopRepl() } - return true } diff --git a/src/main/kotlin/com/jetbrains/micropython/repl/ToolWindowReplTab.kt b/src/main/kotlin/com/jetbrains/micropython/repl/ToolWindowReplTab.kt index ec04d10..98edcde 100644 --- a/src/main/kotlin/com/jetbrains/micropython/repl/ToolWindowReplTab.kt +++ b/src/main/kotlin/com/jetbrains/micropython/repl/ToolWindowReplTab.kt @@ -1,57 +1,63 @@ package com.jetbrains.micropython.repl import com.intellij.icons.AllIcons +import com.intellij.ide.ActivityTracker import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.* import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.components.service import com.intellij.openapi.module.Module -import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.util.NlsContexts +import com.intellij.util.application import com.jediterm.terminal.TtyConnector import com.jetbrains.micropython.settings.MicroPythonDevicesConfiguration +import com.jetbrains.micropython.settings.MicroPythonFacet +import com.jetbrains.micropython.settings.microPythonFacet import org.jetbrains.plugins.terminal.JBTerminalSystemSettingsProvider +import org.jetbrains.plugins.terminal.LocalTerminalDirectRunner +import org.jetbrains.plugins.terminal.ShellStartupOptions import org.jetbrains.plugins.terminal.ShellTerminalWidget import java.awt.BorderLayout +import java.util.concurrent.TimeUnit import javax.swing.JPanel -class ToolWindowReplTab(val module: Module, parent: Disposable) : CommsEventListener, Disposable { +class ToolWindowReplTab(val module: Module, parent: Disposable) : MicroPythonReplControl { private val deviceConfiguration = MicroPythonDevicesConfiguration.getInstance(module.project) - private val deviceCommsManager = MicroPythonReplManager.getInstance(module) val terminalWidget: ShellTerminalWidget init { val mySettingsProvider = JBTerminalSystemSettingsProvider() terminalWidget = ShellTerminalWidget(module.project, mySettingsProvider, parent) terminalWidget.isEnabled = false - deviceCommsManager.addListener(this) + module.project.messageBus.connect(parent).subscribe(MICROPYTHON_REPL_CONTROL, this) + } private fun connectWidgetTty(terminalWidget: ShellTerminalWidget, connector: TtyConnector) { - if (terminalWidget.isSessionRunning) { - terminalWidget.stop() - } terminalWidget.start(connector) val modalityState = ModalityState.stateForComponent(terminalWidget.component) ApplicationManager.getApplication().invokeLater( - { - try { - terminalWidget.component.revalidate() - terminalWidget.notifyStarted() - } catch (e: RuntimeException) { - TODO("You can't cut back on error reporting! You will regret this!") - } - }, - modalityState + { + try { + terminalWidget.component.revalidate() + terminalWidget.notifyStarted() + } catch (e: RuntimeException) { + TODO("You can't cut back on error reporting! You will regret this!") + } + }, + modalityState ) } fun createUI(): JPanel { val actionManager = ActionManager.getInstance() val toolbarActions = DefaultActionGroup().apply { - add(replStartAction()) - add(replStopAction()) - add(clearReplOnLaunch()) + add(replStartAction) + add(replStopAction) + add(clearReplOnLaunch) } val actionToolbar = actionManager.createActionToolbar("MicroPythonREPL", toolbarActions, false) actionToolbar.targetComponent = terminalWidget.component @@ -64,39 +70,36 @@ class ToolWindowReplTab(val module: Module, parent: Disposable) : CommsEventList } } - private fun replStopAction() = object : AnAction( - "Stop", "Stop REPL session", AllIcons.Actions.Suspend - ), DumbAware { + private val replStopAction = object : DumbAwareAction( + "Stop", "Stop REPL session", AllIcons.Actions.Suspend + ) { override fun update(e: AnActionEvent) { - e.presentation.isEnabled = deviceCommsManager.isRunning + e.presentation.isEnabled = terminalWidget.isSessionRunning } override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT override fun actionPerformed(e: AnActionEvent) { - terminalWidget.stop() - deviceCommsManager.stopREPL() + stopRepl() } } - private fun replStartAction() = - object : AnAction("Restart", "Restart REPL session", AllIcons.Actions.Restart), DumbAware { + private val replStartAction = + object : DumbAwareAction("Restart", "Restart REPL session", AllIcons.Actions.Restart) { override fun update(e: AnActionEvent) { e.presentation.isEnabled = true } override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT - override fun actionPerformed(e: AnActionEvent) { - if (terminalWidget.isSessionRunning) { - deviceCommsManager.stopREPL() - } - deviceCommsManager.startREPL() - } + override fun actionPerformed(e: AnActionEvent) = + module.project.service().startOrRestartRepl() } - private fun clearReplOnLaunch() = object : ToggleAction("Clear Window On Start", - "Clear REPL window on every start", AllIcons.Actions.GC) { + private val clearReplOnLaunch = object : ToggleAction( + "Clear Window On Start", + "Clear REPL window on every start", AllIcons.Actions.GC + ) { override fun isSelected(e: AnActionEvent): Boolean { return deviceConfiguration.clearReplOnLaunch } @@ -110,31 +113,88 @@ class ToolWindowReplTab(val module: Module, parent: Disposable) : CommsEventList } } - override fun dispose() { - deviceCommsManager.removeListener(this) + private fun onProcessCreationFailed(@NlsContexts.SystemNotificationText reason: String) { + terminalWidget.terminal.nextLine() + terminalWidget.terminal.writeCharacters(reason) } - override fun onProcessStarted(ttyConnector: TtyConnector) { - if (deviceConfiguration.clearReplOnLaunch) { - terminalWidget.terminalTextBuffer.clearHistory() - terminalWidget.terminal.reset() - } else { - terminalWidget.terminal.nextLine() + private fun interruptBanner() { + application.invokeLater( + { + with(terminalWidget.terminal) { + nextLine() + writeCharacters("=== SESSION HAS BEEN INTERRUPTED ===") + nextLine() + } + }, + { module.isDisposed } + ) + + } + override fun stopRepl() { + interruptBanner() + application.executeOnPooledThread { + synchronized(this) { + terminalWidget.processTtyConnector?.process?.destroy() + } } - connectWidgetTty(terminalWidget, ttyConnector) - terminalWidget.isEnabled = true } - override fun onProcessDestroyed() { - terminalWidget.stop() - - terminalWidget.terminal.nextLine() - terminalWidget.terminal.writeCharacters("=== SESSION HAS BEEN INTERRUPTED ===") - terminalWidget.terminal.nextLine() + override fun startOrRestartRepl() { + interruptBanner() + application.executeOnPooledThread { + synchronized(this) { + terminalWidget.processTtyConnector?.process?.apply { + if (isAlive) destroy() + waitFor(10, TimeUnit.SECONDS) + } + } + application.invokeLater( + { startRepl() }, + { module.project.isDisposed }) + } } - override fun onProcessCreationFailed(reason: String) { - terminalWidget.terminal.nextLine() - terminalWidget.terminal.writeCharacters(reason) + private fun startRepl() { + val facet = module.microPythonFacet ?: return + val devicePath = facet.getOrDetectDevicePathSynchronously() + + if (facet.pythonPath == null) { + onProcessCreationFailed("Valid Python interpreter is needed to start REPL!") + return + } + + if (devicePath == null) { + onProcessCreationFailed("Device path is not specified, please check settings.") + return + } + + val initialShellCommand = mutableListOf( + facet.pythonPath!!, + "${MicroPythonFacet.scriptsPath}/microrepl.py", + devicePath + ) + + val terminalRunner = LocalTerminalDirectRunner(module.project) + + synchronized(this) { + val terminalOptions = terminalRunner.configureStartupOptions( + ShellStartupOptions.Builder() + .shellCommand(initialShellCommand) + .build() + ) + + val process = terminalRunner.createProcess(terminalOptions) + val ttyConnector = terminalRunner.createTtyConnector(process) + process.onExit().whenComplete { _, _ -> ActivityTracker.getInstance().inc() } + if (deviceConfiguration.clearReplOnLaunch) { + terminalWidget.terminalTextBuffer.clearHistory() + terminalWidget.terminal.reset() + } else { + terminalWidget.terminal.nextLine() + } + connectWidgetTty(terminalWidget, ttyConnector) + terminalWidget.isEnabled = true + } } } diff --git a/src/main/kotlin/com/jetbrains/micropython/run/MicroPythonRunConfiguration.kt b/src/main/kotlin/com/jetbrains/micropython/run/MicroPythonRunConfiguration.kt index 13eca7b..b3909e4 100644 --- a/src/main/kotlin/com/jetbrains/micropython/run/MicroPythonRunConfiguration.kt +++ b/src/main/kotlin/com/jetbrains/micropython/run/MicroPythonRunConfiguration.kt @@ -30,6 +30,7 @@ import com.intellij.execution.runners.ProgramRunner import com.intellij.facet.ui.ValidationResult import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.options.ShowSettingsUtil @@ -39,7 +40,6 @@ import com.intellij.openapi.util.Key import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.wm.ToolWindowManager -import com.intellij.serviceContainer.ComponentManagerImpl import com.intellij.util.PathUtil import com.intellij.util.PlatformUtils import com.jetbrains.micropython.repl.MicroPythonReplManager @@ -87,7 +87,7 @@ class MicroPythonRunConfiguration(project: Project, factory: ConfigurationFactor if (runReplOnSuccess && state != null) { return RunStateWrapper(state) { ApplicationManager.getApplication().invokeLater { - MicroPythonReplManager.getInstance(currentModule).startREPL() + project.service().startOrRestartRepl() ToolWindowManager.getInstance(project).getToolWindow("MicroPython")?.show() } } diff --git a/src/main/kotlin/com/jetbrains/micropython/ui/MicroPythonToolWindowFactory.kt b/src/main/kotlin/com/jetbrains/micropython/ui/MicroPythonToolWindowFactory.kt index f0fe6f4..f08a2ca 100644 --- a/src/main/kotlin/com/jetbrains/micropython/ui/MicroPythonToolWindowFactory.kt +++ b/src/main/kotlin/com/jetbrains/micropython/ui/MicroPythonToolWindowFactory.kt @@ -1,5 +1,6 @@ package com.jetbrains.micropython.ui +import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow @@ -16,12 +17,9 @@ class MicroPythonToolWindowFactory : ToolWindowFactory, DumbAware { project.firstMicroPythonFacet?.let { terminalContent.component = ToolWindowReplTab(it.module, terminalContent).createUI() - } - - toolWindow.contentManager.addContent(terminalContent) - - project.firstMicroPythonFacet?.let { - MicroPythonReplManager.getInstance(it.module).startREPL() + toolWindow.contentManager.addContent(terminalContent) + project.service().startOrRestartRepl() } } + } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 649a271..da78b0b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -42,7 +42,6 @@ displayName="MicroPython" instance="com.jetbrains.micropython.settings.MicroPythonProjectConfigurable"/> -