src/org/jetbrains/r/rmarkdown/RMarkdownRenderingConsoleRunner.kt (146 lines of code) (raw):

/* * Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. */ package org.jetbrains.r.rmarkdown import com.intellij.execution.executors.DefaultRunExecutor import com.intellij.execution.impl.ConsoleViewImpl import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessListener import com.intellij.execution.process.ScriptRunnerUtil import com.intellij.execution.ui.ConsoleViewContentType import com.intellij.execution.ui.RunContentDescriptor import com.intellij.execution.ui.RunContentManager import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.EDT import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile import com.intellij.r.psi.RBundle import com.intellij.r.psi.RPluginUtil import com.intellij.r.psi.interpreter.RInterpreter import com.intellij.r.psi.interpreter.RInterpreterManager import com.intellij.r.psi.interpreter.runHelperProcess import com.intellij.r.psi.interpreter.uploadFileToHost import com.intellij.r.psi.util.RPathUtil import com.intellij.util.PathUtil import kotlinx.coroutines.* import org.jetbrains.r.rendering.settings.RMarkdownSettings import java.awt.BorderLayout import java.io.IOException import javax.swing.JPanel import kotlin.coroutines.resumeWithException class RMarkdownRenderingConsoleRunnerException(message: String) : RuntimeException(message) class RMarkdownRenderingConsoleRunner( private val project: Project, private val consoleTitle: String = RBundle.message("rmarkdown.rendering.console.title"), ) { @Volatile private var isInterrupted = false @Volatile private var currentProcessHandler: ProcessHandler? = null @Volatile private var currentConsoleView: ConsoleViewImpl? = null fun interruptRendering() { isInterrupted = true currentConsoleView?.print("\nInterrupted\n", ConsoleViewContentType.ERROR_OUTPUT) currentProcessHandler?.let { ScriptRunnerUtil.terminateProcessHandler(it, 2000, null) } } @Throws(RMarkdownRenderingConsoleRunnerException::class, IOException::class) suspend fun render(project: Project, file: VirtualFile, isShiny: Boolean = false) { RMarkdownUtil.checkOrInstallPackages(project, RBundle.message("rmarkdown.processor.notification.utility.name")) if (!isInterrupted) { doRender(project, file, isShiny) } else { throw RMarkdownRenderingConsoleRunnerException("Rendering was interrupted") } } private fun createConsoleView(processHandler: ProcessHandler): ConsoleViewImpl { val consoleView = ConsoleViewImpl(project, false) currentConsoleView = consoleView val rMarkdownConsolePanel = JPanel(BorderLayout()) rMarkdownConsolePanel.add(consoleView.component, BorderLayout.CENTER) val title = consoleTitle val contentDescriptor = RunContentDescriptor(consoleView, processHandler, rMarkdownConsolePanel, title) contentDescriptor.setFocusComputable { consoleView.preferredFocusableComponent } contentDescriptor.isAutoFocusContent = true val contentManager = RunContentManager.getInstance(project) val executor = DefaultRunExecutor.getRunExecutorInstance() contentManager.allDescriptors.filter { it.displayName == title }.forEach { contentManager.removeRunContent(executor, it) } contentManager.showRunContent(executor, contentDescriptor) return consoleView } private suspend fun doRender(project: Project, rMarkdownFile: VirtualFile, isShiny: Boolean) { val interpreter = RInterpreterManager.getInstance(project).awaitInterpreter().getOrThrow() interpreter.prepareForExecution() val rmdFileOnHost = interpreter.uploadFileToHostIfNeeded(rMarkdownFile, preserveName = true) val knitRootDirectory = PathUtil.getParentPath(rmdFileOnHost) val outputDirectory = RMarkdownSettings.getInstance(project).state.getOutputDirectory(rMarkdownFile) ?: rMarkdownFile.parent val outputDirOnHost = interpreter.getFilePathAtHost(outputDirectory) ?: knitRootDirectory val libraryPath = RPathUtil.join(interpreter.getHelpersRootOnHost(), "pandoc") val resultTmpFile = interpreter.createTempFileOnHost("rmd-output-path.txt") val args = arrayListOf(libraryPath, rmdFileOnHost, knitRootDirectory, outputDirOnHost, resultTmpFile) val processHandler = interpreter.runHelperProcess(interpreter.uploadFileToHost(R_MARKDOWN_HELPER), args, knitRootDirectory) currentProcessHandler = processHandler coroutineScope { suspendCancellableCoroutine { cancellableContinuation -> val knitListener = makeKnitListener(interpreter, rMarkdownFile, resultTmpFile, outputDirectory, isShiny, cancellableContinuation) processHandler.addProcessListener(knitListener) launch(Dispatchers.EDT) { if (!ApplicationManager.getApplication().isUnitTestMode) { val consoleView = createConsoleView(processHandler) consoleView.attachToProcess(processHandler) consoleView.scrollToEnd() } processHandler.startNotify() } } } } private fun renderingErrorNotification() { Notification("RMarkdownRenderError", RBundle.message("notification.title.rendering.status"), RBundle.message("notification.content.error.occurred.during.rendering"), NotificationType.ERROR) .notify(project) } private fun makeKnitListener( interpreter: RInterpreter, file: VirtualFile, resultTmpFileOnHost: String, outputDir: VirtualFile, isShiny: Boolean, cancellableContinuation: CancellableContinuation<Unit>, ): ProcessListener = object : ProcessListener { override fun processTerminated(event: ProcessEvent) { val exitCode = event.exitCode if (isInterrupted || currentConsoleView?.let { Disposer.isDisposed(it) } == true) { isInterrupted = false cancellableContinuation.resumeWithException(RMarkdownRenderingConsoleRunnerException("Rendering was interrupted")) } else { if (exitCode == 0) { try { if (!isShiny) { val outputPathOnHost = interpreter.findFileByPathAtHost(resultTmpFileOnHost)?.contentsToByteArray()?.toString(Charsets.UTF_8).orEmpty() RMarkdownSettings.getInstance(project).state.setProfileLastOutput(file, outputPathOnHost) outputDir.refresh(true, false) } cancellableContinuation.resume(Unit) { cause, _, _ -> Unit } } catch (e: IOException) { cancellableContinuation.resumeWithException(e) } } else { renderingErrorNotification() cancellableContinuation.resumeWithException(RMarkdownRenderingConsoleRunnerException("Rendering has non-zero exit code")) } } } } companion object { private val R_MARKDOWN_HELPER = RPluginUtil.findFileInRHelpers("R/render_markdown.R") } }