diff --git a/python/python-process-output/frontend/src/ProcessOutputControllerService.kt b/python/python-process-output/frontend/src/ProcessOutputControllerService.kt index 47c409f0ae9e..301a84a7c193 100644 --- a/python/python-process-output/frontend/src/ProcessOutputControllerService.kt +++ b/python/python-process-output/frontend/src/ProcessOutputControllerService.kt @@ -34,6 +34,7 @@ import com.intellij.python.processOutput.frontend.ui.components.OutputSectionTes import com.intellij.python.processOutput.frontend.ui.components.TreeSectionTestTags import com.intellij.python.processOutput.frontend.ui.shortenedCommandString import java.util.WeakHashMap +import kotlin.enums.enumEntries import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Instant import kotlinx.coroutines.CoroutineName @@ -284,6 +285,8 @@ internal class ProcessOutputControllerService( val showTags = processOutputUiState.filters.active.contains(OutputFilter.Item.SHOW_TAGS) val stringToCopy = buildString { + var lastTag: Tag? = null + loggedProcess.lines.forEach { line -> if (showTags) { val tag = when (line.kind) { @@ -291,11 +294,13 @@ internal class ProcessOutputControllerService( OutputKindDto.ERR -> Tag.ERROR } - append("[$tag] ".padStart(Tag.maxLength + 3)) - } else { - repeat(Tag.maxLength + 3) { - append(' ') + if (lastTag == tag) { + append(Tag.blankBracketTagString) + } else { + append(tag.bracketTagString) } + + lastTag = tag } appendLine(line.text) @@ -307,7 +312,10 @@ internal class ProcessOutputControllerService( } exitData?.also { exitData -> - append("[${Tag.EXIT}] ".padStart(Tag.maxLength + 3)) + if (showTags) { + append(Tag.EXIT.bracketTagString) + } + append(exitData.exitCode) exitData.additionalMessageToUser?.also { message -> @@ -736,15 +744,27 @@ internal class ProcessOutputControllerService( } } -internal object Tag { - val ERROR = message("process.output.output.tag.stdout") - val OUTPUT = message("process.output.output.tag.stderr") - val EXIT = message("process.output.output.tag.exit") +internal enum class Tag(val text: String) { + ERROR(message("process.output.output.tag.stderr")), + OUTPUT(message("process.output.output.tag.stdout")), + EXIT(message("process.output.output.tag.exit")); - val maxLength: Int = - Tag::class.java.declaredFields - .filter { it.type == String::class.java } - .fastMaxOfOrDefault(0) { (it.get(null) as String).length } + override fun toString(): String = text + + val bracketTagString: String + get() = "[$text] ".padStart(bracketPadding) + + val colonTagString: String + get() = "$text:".padStart(colonPadding) + + companion object { + private val maxLength = enumEntries().fastMaxOfOrDefault(0) { it.text.length } + private val bracketPadding = maxLength + 3 // opening bracket, closing bracket, and space + private val colonPadding = maxLength + 1 // colon + + val blankBracketTagString: String = " ".repeat(bracketPadding) + val blankColonTagString: String = " ".repeat(colonPadding) + } } private fun boundedLinkedHashMap(maxSize: Int): LinkedHashMap = diff --git a/python/python-process-output/frontend/src/ui/components/OutputSection.kt b/python/python-process-output/frontend/src/ui/components/OutputSection.kt index 8c0f3541ff0e..75f4bcde9a6c 100644 --- a/python/python-process-output/frontend/src/ui/components/OutputSection.kt +++ b/python/python-process-output/frontend/src/ui/components/OutputSection.kt @@ -260,15 +260,8 @@ private fun OutputLine( ) { if (displayTags) { DisableSelection { - val padding = Tag.maxLength + 1 - Text( - text = - if (sectionIndicator != null) { - "${sectionIndicator.tag}:".padStart(padding, ' ') - } else { - " ".repeat(padding) - }, + text = sectionIndicator?.tag?.colonTagString ?: Tag.blankColonTagString, style = JewelTheme.consoleTextStyle, fontWeight = FontWeight.Thin, modifier = @@ -309,7 +302,7 @@ private fun OutputLine( } private data class SectionIndicator( - val tag: String, + val tag: Tag, val copyButtonTestTag: String = OutputSectionTestTags.COPY_OUTPUT_TAG_SECTION_BUTTON, val onCopy: () -> Unit, ) @@ -430,8 +423,8 @@ private sealed class InfoLine { private val OutputKindDto.tag get() = when (this) { - OutputKindDto.OUT -> Tag.ERROR - OutputKindDto.ERR -> Tag.OUTPUT + OutputKindDto.OUT -> Tag.OUTPUT + OutputKindDto.ERR -> Tag.ERROR } internal object OutputSectionTestTags { diff --git a/python/python-process-output/frontend/test/junit5Tests/env/ProcessOutputControllerServiceTest.kt b/python/python-process-output/frontend/test/junit5Tests/env/ProcessOutputControllerServiceTest.kt index 6a372c79304f..b1ccc45348ea 100644 --- a/python/python-process-output/frontend/test/junit5Tests/env/ProcessOutputControllerServiceTest.kt +++ b/python/python-process-output/frontend/test/junit5Tests/env/ProcessOutputControllerServiceTest.kt @@ -7,15 +7,18 @@ import com.intellij.python.community.execService.Args import com.intellij.python.community.execService.BinOnEel import com.intellij.python.community.execService.ExecService import com.intellij.python.community.execService.impl.LoggingLimits +import com.intellij.python.community.execService.impl.LoggingProcess import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath import com.intellij.python.processOutput.common.OutputKindDto import com.intellij.python.processOutput.common.OutputLineDto import com.intellij.python.processOutput.frontend.CoroutineNames import com.intellij.python.processOutput.frontend.LoggedProcess +import com.intellij.python.processOutput.frontend.OutputFilter import com.intellij.python.processOutput.frontend.ProcessOutputControllerService import com.intellij.python.processOutput.frontend.ProcessOutputControllerServiceLimits import com.intellij.python.processOutput.frontend.ProcessStatus +import com.intellij.python.processOutput.frontend.ui.toggle import com.intellij.testFramework.common.timeoutRunBlocking import com.intellij.testFramework.common.waitUntil import com.intellij.testFramework.junit5.fixture.projectFixture @@ -374,6 +377,106 @@ class ProcessOutputControllerServiceTest { ) } + @Test + fun `toolbar copy includes tags depending on whether the filter is enabled`( + @TempDir cwd: Path, + @PythonBinaryPath python: PythonBinary, + ): Unit = timeoutRunBlocking { + val service = projectFixture.get().service() + + val binOnEel = BinOnEel(python, cwd) + val mainPy = Files.createFile(cwd.resolve(MAIN_PY)) + + edtWriteAction { + mainPy.toFile().writeText( + """ + import sys + + print("out1") + print("out2") + print("out3") + print("out4") + print("out5") + print("out6") + + print("err7", file=sys.stderr) + print("err8", file=sys.stderr) + print("err9", file=sys.stderr) + print("err10", file=sys.stderr) + """.trimIndent(), + ) + } + + val loggingProcess = withContext(NON_INTERACTIVE_ROOT_TRACE_CONTEXT) { + ExecService().executeGetProcess( + binOnEel, + Args(MAIN_PY), + CoroutineScope(coroutineContext), + ).getOrThrow() + } + + // reading all stdout + loggingProcess.inputStream.readAllBytes() + + waitUntil { + service.loggedProcesses.value.lastOrNull()?.lines?.size == 6 + } + + val process = service.loggedProcesses.value.last() + + // reading all stderr + loggingProcess.errorStream.readAllBytes() + + waitUntil { + service.loggedProcesses.value.lastOrNull()?.lines?.size == 10 + } + + // copying output + service.copyOutputToClipboard(process) + + // copied output should include tags + assertEquals( + """ + [stdout] out1 + out2 + out3 + out4 + out5 + out6 + [stderr] err7 + err8 + err9 + err10 + [exit] 0 + + """.trimIndent(), + CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor), + ) + + // toggling the show tags filter + service.processOutputUiState.filters.active.toggle(OutputFilter.Item.SHOW_TAGS) + service.copyOutputToClipboard(process) + + // copied output should not include tags + waitUntil("output without tags") { + CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor) == + """ + out1 + out2 + out3 + out4 + out5 + out6 + err7 + err8 + err9 + err10 + 0 + + """.trimIndent() + } + } + @Test fun `non-ascii output lines are reflected properly`( @TempDir cwd: Path,