potw-changes-to-261

This MR cherry picks changes done for PY-87723 and PY-87578

Merge-request: IJ-MR-193482
Merged-by: David Lysenko <david.lysenko@jetbrains.com>

GitOrigin-RevId: c74229ed48ae8702667dda201613a2d0c3f25369
This commit is contained in:
David Lysenko
2026-02-26 19:55:47 +00:00
committed by intellij-monorepo-bot
parent 0425113837
commit c23c78ac0d
102 changed files with 2336 additions and 1676 deletions

5
.idea/modules.xml generated
View File

@@ -1493,8 +1493,9 @@
<module fileurl="file://$PROJECT_DIR$/python/python-markdown/intellij.python.markdown.iml" filepath="$PROJECT_DIR$/python/python-markdown/intellij.python.markdown.iml" />
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.ml.features/intellij.python.ml.features.iml" filepath="$PROJECT_DIR$/python/intellij.python.ml.features/intellij.python.ml.features.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-parser/intellij.python.parser.iml" filepath="$PROJECT_DIR$/python/python-parser/intellij.python.parser.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-process-output/intellij.python.processOutput.iml" filepath="$PROJECT_DIR$/python/python-process-output/intellij.python.processOutput.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-process-output/impl/intellij.python.processOutput.impl.iml" filepath="$PROJECT_DIR$/python/python-process-output/impl/intellij.python.processOutput.impl.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-process-output/backend/intellij.python.processOutput.backend.iml" filepath="$PROJECT_DIR$/python/python-process-output/backend/intellij.python.processOutput.backend.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-process-output/common/intellij.python.processOutput.common.iml" filepath="$PROJECT_DIR$/python/python-process-output/common/intellij.python.processOutput.common.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-process-output/frontend/intellij.python.processOutput.frontend.iml" filepath="$PROJECT_DIR$/python/python-process-output/frontend/intellij.python.processOutput.frontend.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-psi-api/intellij.python.psi.iml" filepath="$PROJECT_DIR$/python/python-psi-api/intellij.python.psi.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-psi-impl/intellij.python.psi.impl.iml" filepath="$PROJECT_DIR$/python/python-psi-impl/intellij.python.psi.impl.iml" />
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.pydev.iml" filepath="$PROJECT_DIR$/python/intellij.python.pydev.iml" />

View File

@@ -385,7 +385,7 @@ jvm_library(
"//platform/testFramework/monorepo",
"//platform/testFramework/monorepo:monorepo_test_lib",
"//plugins/textmate/tests:tests_test_lib",
"//python/python-process-output:processOutput",
"//python/python-process-output/common",
"//tools/intellij.tools.ide.starter.junit5:ide-starter-junit5",
"//tools/intellij.tools.ide.starter.junit5:ide-starter-junit5_test_lib",
"//platform/build-scripts/product-dsl",

View File

@@ -1543,8 +1543,9 @@ python/python-markdown
python/python-parser
python/python-poetry/backend
python/python-poetry/common
python/python-process-output
python/python-process-output/impl
python/python-process-output/backend
python/python-process-output/common
python/python-process-output/frontend
python/python-psi-api
python/python-psi-impl
python/python-pyproject

View File

@@ -259,7 +259,7 @@
<orderEntry type="module" module-name="intellij.maven.server.m40" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.mcpserver" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.textmate.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.processOutput" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" scope="TEST" />
<orderEntry type="module" module-name="intellij.tools.ide.starter.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.buildScripts.productDsl" scope="TEST" />
<orderEntry type="module" module-name="intellij.terminal.tests" scope="TEST" />

View File

@@ -312,8 +312,8 @@ jvm_library(
"//python/python-poetry/common",
"//python/python-uv/common",
"//python/common",
"//python/python-process-output:processOutput",
"//platform/xdebugger-impl/shared",
"//python/python-process-output/common",
"//platform/xdebugger-impl/rpc",
"//platform/xdebugger-impl/ui",
"//platform/execution",

View File

@@ -1,31 +1,32 @@
<idea-plugin visibility="public">
<dependencies>
<module name="intellij.python.parser"/>
<module name="intellij.python.ast"/>
<module name="intellij.python.syntax.core"/>
<module name="intellij.python.syntax"/>
<module name="intellij.python.psi"/>
<module name="intellij.python.psi.impl"/>
<module name="intellij.python.sdk"/>
<module name="intellij.python.sdk.ui"/>
<module name="intellij.python.pyproject"/>
<module name="intellij.python.community.impl.poetry.common"/>
<module name="intellij.python.community.impl.pipenv"/>
<module name="intellij.python.community.core.impl"/>
<module name="intellij.python.community.helpersLocator"/>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.python.ast"/>
<module name="intellij.python.community"/>
<module name="intellij.python.community.impl"/>
<module name="intellij.python.community.core.impl"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.community.execService.python"/>
<module name="intellij.python.community.helpersLocator"/>
<module name="intellij.python.community.impl"/>
<module name="intellij.python.community.impl.installer"/>
<module name="intellij.python.pydev"/>
<module name="intellij.python.venv"/>
<module name="intellij.python.hatch"/>
<module name="intellij.python.community.services.shared"/>
<module name="intellij.python.community.services.internal.impl"/>
<module name="intellij.python.community.services.systemPython"/>
<module name="intellij.python.community.interpreters"/>
<module name="intellij.python.processOutput"/>
<module name="intellij.python.community.services.internal.impl"/>
<module name="intellij.python.community.services.shared"/>
<module name="intellij.python.community.services.systemPython"/>
<module name="intellij.python.hatch"/>
<module name="intellij.python.parser"/>
<module name="intellij.python.psi"/>
<module name="intellij.python.psi.impl"/>
<module name="intellij.python.pydev"/>
<module name="intellij.python.pyproject"/>
<module name="intellij.python.sdk"/>
<module name="intellij.python.sdk.ui"/>
<module name="intellij.python.syntax"/>
<module name="intellij.python.syntax.core"/>
<module name="intellij.python.venv"/>
<!-- endregion -->
</dependencies>
<!-- Declare that we support core Python functionality -->
<module value="com.intellij.modules.python"/>

View File

@@ -17,8 +17,8 @@ jvm_library(
"@lib//:kotlin-stdlib",
"@lib//:jetbrains-annotations",
"//platform/core-api:core",
"//python/python-process-output:processOutput",
"//platform/util",
"//python/python-process-output/common",
]
)
### auto-generated section `build intellij.python.community.impl.conda` end

View File

@@ -12,7 +12,7 @@
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.python.processOutput" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
</component>
</module>

View File

@@ -1,8 +1,8 @@
<idea-plugin visibility="internal">
<dependencies>
<module name="intellij.python.processOutput"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<python.processOutput.processOutputIconMapping implementation="com.intellij.python.community.impl.conda.CondaIconMapping"/>
<python.processOutput.common.processOutputIconMapping implementation="com.intellij.python.community.impl.conda.CondaIconMapping"/>
</extensions>
</idea-plugin>

View File

@@ -2,9 +2,9 @@
package com.intellij.python.community.impl.conda
import com.intellij.python.community.impl.conda.icons.PythonCommunityImplCondaIcons
import com.intellij.python.processOutput.ProcessBinaryFileName
import com.intellij.python.processOutput.ProcessIcon
import com.intellij.python.processOutput.ProcessOutputIconMapping
import com.intellij.python.processOutput.common.ProcessBinaryFileName
import com.intellij.python.processOutput.common.ProcessIcon
import com.intellij.python.processOutput.common.ProcessOutputIconMapping
internal class CondaIconMapping : ProcessOutputIconMapping() {
override val mapping: Map<ProcessBinaryFileName, ProcessIcon> = mapOf(

View File

@@ -191,8 +191,8 @@
<orderEntry type="module" module-name="intellij.python.community.impl.poetry.common" />
<orderEntry type="module" module-name="intellij.python.community.impl.uv.common" />
<orderEntry type="module" module-name="intellij.python.common" />
<orderEntry type="module" module-name="intellij.python.processOutput" />
<orderEntry type="module" module-name="intellij.platform.debugger.impl.shared" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
<orderEntry type="module" module-name="intellij.platform.debugger.impl.rpc" />
<orderEntry type="module" module-name="intellij.platform.debugger.impl.ui" />
<orderEntry type="module" module-name="intellij.platform.execution" />

View File

@@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.currentCoroutineContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.util.UUID
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
@@ -14,8 +15,9 @@ class TraceContext(
val parentTraceContext: TraceContext?,
) : AbstractCoroutineContextElement(TraceContext) {
val timestamp: Long = System.currentTimeMillis()
val uuid: UUID = UUID.randomUUID()
constructor(title: @Nls String, coroutineScope: CoroutineScope): this(title, coroutineScope.coroutineContext[Key])
constructor(title: @Nls String, coroutineScope: CoroutineScope) : this(title, coroutineScope.coroutineContext[Key])
override fun toString(): String = "TraceContext($title, $timestamp)\n\t-> $parentTraceContext"

View File

@@ -19,7 +19,7 @@ jvm_library(
"//platform/core-api:core",
"//python/python-sdk:sdk",
"//platform/util",
"//python/python-process-output:processOutput",
"//python/python-process-output/common",
]
)
### auto-generated section `build intellij.python.community.impl.pipenv` end

View File

@@ -14,6 +14,6 @@
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.python.processOutput" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
</component>
</module>

View File

@@ -1,9 +1,9 @@
<idea-plugin visibility="internal">
<dependencies>
<module name="intellij.python.sdk"/>
<module name="intellij.python.processOutput"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<python.processOutput.processOutputIconMapping implementation="com.intellij.python.community.impl.pipenv.PipenvIconMapping"/>
<python.processOutput.common.processOutputIconMapping implementation="com.intellij.python.community.impl.pipenv.PipenvIconMapping"/>
</extensions>
</idea-plugin>

View File

@@ -2,9 +2,9 @@
package com.intellij.python.community.impl.pipenv
import com.intellij.python.community.impl.pipenv.icons.PythonCommunityImplPipenvIcons
import com.intellij.python.processOutput.ProcessBinaryFileName
import com.intellij.python.processOutput.ProcessIcon
import com.intellij.python.processOutput.ProcessOutputIconMapping
import com.intellij.python.processOutput.common.ProcessBinaryFileName
import com.intellij.python.processOutput.common.ProcessIcon
import com.intellij.python.processOutput.common.ProcessOutputIconMapping
internal class PipenvIconMapping : ProcessOutputIconMapping() {
override val mapping: Map<ProcessBinaryFileName, ProcessIcon> = mapOf(

View File

@@ -129,12 +129,15 @@
- name: lib/modules/intellij.python.parser.jar
contentModules:
- name: intellij.python.parser
- name: lib/modules/intellij.python.processOutput.impl.jar
- name: lib/modules/intellij.python.processOutput.backend.jar
contentModules:
- name: intellij.python.processOutput.impl
- name: lib/modules/intellij.python.processOutput.jar
- name: intellij.python.processOutput.backend
- name: lib/modules/intellij.python.processOutput.common.jar
contentModules:
- name: intellij.python.processOutput
- name: intellij.python.processOutput.common
- name: lib/modules/intellij.python.processOutput.frontend.jar
contentModules:
- name: intellij.python.processOutput.frontend
- name: lib/modules/intellij.python.psi.impl.jar
contentModules:
- name: intellij.python.psi.impl

View File

@@ -38,6 +38,9 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<module name="intellij.python.sdkConfigurator.common" loading="required"/>
<module name="intellij.python.sdkConfigurator.backend"/>
<module name="intellij.python.sdkConfigurator.frontend"/>
<module name="intellij.python.processOutput.common" loading="required"/>
<module name="intellij.python.processOutput.backend" required-if-available="intellij.platform.backend"/>
<module name="intellij.python.processOutput.frontend" required-if-available="intellij.platform.frontend"/>
<module name="intellij.python.community.impl.poetry.common" loading="required"/>
<module name="intellij.python.community.impl.poetry.backend"/>
<module name="intellij.python.community.impl.uv.common" loading="required"/>
@@ -59,8 +62,6 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<module name="intellij.python.community.services.systemPython" loading="required"/>
<module name="intellij.python.community.interpreters" loading="required"/>
<module name="intellij.python.community.aliasProvider" loading="required"/>
<module name="intellij.python.processOutput" loading="required"/>
<module name="intellij.python.processOutput.impl"/>
<module name="intellij.python.externalIndex" loading="required"/>
<!--Mini-IDes support community python only-->
<module name="intellij.python.community.plugin.minor"/> <!-- Python for Mini-IDEs-->

View File

@@ -25,7 +25,7 @@
<module name="intellij.python.venv"/>
<module name="intellij.python.community.services.systemPython"/>
<module name="intellij.python.sdkConfigurator.common"/>
<module name="intellij.python.processOutput"/>
<module name="intellij.python.processOutput.common"/>
<module name="intellij.platform.debugger"/>
<module name="intellij.platform.debugger.impl"/>
</dependencies>
@@ -719,7 +719,7 @@
<!-- ux survey -->
<feedback.idleFeedbackSurvey implementation="com.jetbrains.python.statistics.feedback.PyCharmUxSurvey"/>
<python.processOutput.processOutputIconMapping implementation="com.jetbrains.python.sdk.SdkIconMapping"/>
<python.processOutput.common.processOutputIconMapping implementation="com.jetbrains.python.sdk.SdkIconMapping"/>
<!-- Debugger Show value tooltip -->
<registryKey defaultValue="true" description="Show value tooltip" key="python.debugger.show.value.tooltip"/>

View File

@@ -42,6 +42,7 @@ jvm_library(
"//libraries/kotlinx/serialization/core",
"//platform/remote-servers/impl",
"//libraries/guava",
"//python/python-process-output/common",
]
)
@@ -78,6 +79,7 @@ jvm_library(
"//libraries/kotlinx/serialization/core",
"//platform/remote-servers/impl",
"//libraries/guava",
"//python/python-process-output/common",
]
)
### auto-generated section `build intellij.python.community.execService` end

View File

@@ -53,5 +53,6 @@
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" />
<orderEntry type="module" module-name="intellij.platform.remoteServers.impl" />
<orderEntry type="module" module-name="intellij.libraries.guava" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
</component>
</module>

View File

@@ -2,6 +2,7 @@
<dependencies>
<module name="intellij.python.community"/>
<module name="intellij.python.community.helpersLocator"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<extensionPoints>

View File

@@ -6,19 +6,23 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.python.community.execService.ConcurrentProcessWeight
import com.intellij.python.processOutput.common.ExecutableDto
import com.intellij.python.processOutput.common.LoggedProcessDto
import com.intellij.python.processOutput.common.OutputKindDto
import com.intellij.python.processOutput.common.OutputLineDto
import com.intellij.python.processOutput.common.ProcessOutputEventDto
import com.intellij.python.processOutput.common.ProcessWeightDto
import com.intellij.python.processOutput.common.TraceContextDto
import com.intellij.python.processOutput.common.TraceContextKind
import com.intellij.python.processOutput.common.TraceContextUuid
import com.intellij.python.processOutput.common.sendProcessOutputTopicEvent
import com.intellij.util.io.awaitExit
import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT
import com.jetbrains.python.TraceContext
import com.jetbrains.python.errorProcessing.Exe
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.TimeUnit
@@ -33,72 +37,6 @@ object LoggingLimits {
* The maximum buffer size of a LoggingProcess
*/
const val MAX_OUTPUT_SIZE = 100_000
const val MAX_LINES = 1024
}
@ApiStatus.Internal
data class LoggedProcess(
val weight: ConcurrentProcessWeight?,
val traceContext: TraceContext?,
val pid: Long?,
val startedAt: Instant,
val cwd: String?,
val exe: LoggedProcessExe,
val args: List<String>,
val env: Map<String, String>,
val target: String,
val lines: SharedFlow<LoggedProcessLine>,
val exitInfo: MutableStateFlow<LoggedProcessExitInfo?>,
) {
val id: Int = nextId.getAndAdd(1)
val commandString: String
get() = commandFromSegments(listOf(exe.path) + args)
/**
* Command string with the full path of the exe trimmed only to the latest segments. E.g., `/usr/bin/uv` -> `uv`.
*/
val shortenedCommandString: String
get() = commandFromSegments(listOf(exe.parts.last()) + args)
companion object {
private val nextId: AtomicInteger = AtomicInteger(0)
private fun commandFromSegments(segments: List<String>) =
segments.joinToString(" ")
}
}
@ApiStatus.Internal
data class LoggedProcessExe(
val path: String,
val parts: List<String>,
)
@ApiStatus.Internal
data class LoggedProcessExitInfo(
val exitedAt: Instant,
val exitValue: Int,
val additionalMessageToUser: @Nls String? = null,
val isCritical: Boolean = false,
)
@ApiStatus.Internal
data class LoggedProcessLine(
val text: String,
val kind: Kind,
) {
enum class Kind {
OUT,
ERR
}
}
@ApiStatus.Internal
@Service
class ExecLoggerService(val scope: CoroutineScope) {
internal val processesInternal = MutableSharedFlow<LoggedProcess>()
val processes: Flow<LoggedProcess> = processesInternal.asSharedFlow()
}
@ApiStatus.Internal
@@ -113,51 +51,65 @@ class LoggingProcess(
env: Map<String, String>,
target: String,
) : Process() {
val loggedProcess: LoggedProcess
private val linesFlow = MutableSharedFlow<LoggedProcessLine>(
replay = LoggingLimits.MAX_LINES,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val stdoutStream = LoggingInputStream(backingProcess.inputStream, linesFlow, LoggedProcessLine.Kind.OUT)
private val stderrStream = LoggingInputStream(backingProcess.errorStream, linesFlow, LoggedProcessLine.Kind.ERR)
init {
val service = ApplicationManager.getApplication().service<ExecLoggerService>()
val exitInfoFlow = MutableStateFlow<LoggedProcessExitInfo?>(null)
loggedProcess =
LoggedProcess(
weight,
traceContext,
val loggedProcess: LoggedProcessDto =
LoggedProcessDto(
weight =
when (weight) {
ConcurrentProcessWeight.LIGHT -> ProcessWeightDto.LIGHT
ConcurrentProcessWeight.MEDIUM -> ProcessWeightDto.MEDIUM
ConcurrentProcessWeight.HEAVY -> ProcessWeightDto.HEAVY
null -> null
},
traceContextUuid =
traceContext?.let {
TraceContextUuid(it.uuid.toString())
},
pid =
try {
backingProcess.pid()
}
catch (_: UnsupportedOperationException) {
null
},
startedAt,
cwd,
LoggedProcessExe(
startedAt = startedAt,
cwd = cwd,
exe =
ExecutableDto(
path = exe.toString(),
parts = exe.pathParts(),
),
args,
env,
target,
linesFlow,
exitInfoFlow,
)
args = args,
env = env,
target = target,
id = nextId.getAndAdd(1),
)
private val lineCounter = AtomicInteger(0)
service.scope.launch {
service.processesInternal.emit(loggedProcess)
private val stdoutStream = LoggingInputStream(loggedProcess.id, backingProcess.inputStream, OutputKindDto.OUT, lineCounter)
private val stderrStream = LoggingInputStream(loggedProcess.id, backingProcess.errorStream, OutputKindDto.ERR, lineCounter)
init {
ApplicationManager.getApplication().service<LoggingService>().scope.launch {
val traceHierarchy = mutableListOf<TraceContextDto>()
var currentTraceContext = traceContext
while (currentTraceContext != null) {
traceHierarchy += currentTraceContext.toDto()
currentTraceContext = currentTraceContext.parentTraceContext
}
sendProcessOutputTopicEvent(
ProcessOutputEventDto.NewProcess(loggedProcess, traceHierarchy)
)
awaitExit()
exitInfoFlow.value = LoggedProcessExitInfo(
exitedAt = Clock.System.now(),
exitValue = exitValue(),
sendProcessOutputTopicEvent(
ProcessOutputEventDto.ProcessExit(
processId = loggedProcess.id,
exitedAt = Clock.System.now(),
exitValue = exitValue(),
)
)
}
}
@@ -193,12 +145,17 @@ class LoggingProcess(
override fun supportsNormalTermination(): Boolean =
backingProcess.supportsNormalTermination()
companion object {
private val nextId: AtomicInteger = AtomicInteger(0)
}
}
private class LoggingInputStream(
private val processId: Int,
private val backingInputStream: InputStream,
private val linesFlow: MutableSharedFlow<LoggedProcessLine>,
private val kind: LoggedProcessLine.Kind,
private val kind: OutputKindDto,
private val lineCounter: AtomicInteger,
) : InputStream() {
private var closed = AtomicBoolean(false)
private var outputSize = 0
@@ -286,10 +243,14 @@ private class LoggingInputStream(
private fun finalizeLine(bytes: ByteArray) {
val line = String(bytes)
linesFlow.tryEmit(
LoggedProcessLine(
line,
kind,
sendProcessOutputTopicEvent(
ProcessOutputEventDto.NewOutputLine(
processId = processId,
OutputLineDto(
kind = kind,
text = line,
lineNo = lineCounter.getAndAdd(1),
)
)
)
@@ -304,3 +265,22 @@ private class LoggingInputStream(
}
}
}
private fun TraceContext.toDto(): TraceContextDto =
TraceContextDto(
title = title,
timestamp = timestamp,
uuid = TraceContextUuid(uuid.toString()),
kind =
when (this) {
NON_INTERACTIVE_ROOT_TRACE_CONTEXT -> TraceContextKind.NON_INTERACTIVE
else -> TraceContextKind.INTERACTIVE
},
parentUuid =
parentTraceContext?.let {
TraceContextUuid(it.uuid.toString())
}
)
@Service
private class LoggingService(val scope: CoroutineScope)

View File

@@ -1,6 +1,9 @@
<idea-plugin>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<module name="intellij.platform.testFramework.junit5.eel._test"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<!-- endregion -->
</idea-plugin>

View File

@@ -1,245 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.junit5Tests.unit
import com.intellij.python.community.execService.impl.LoggedProcess
import com.intellij.python.community.execService.impl.LoggedProcessExe
import com.intellij.python.community.execService.impl.LoggedProcessLine
import com.intellij.python.community.execService.impl.LoggingLimits
import com.intellij.python.community.execService.impl.LoggingProcess
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.common.waitUntil
import com.intellij.testFramework.junit5.TestApplication
import com.jetbrains.python.TraceContext
import com.jetbrains.python.errorProcessing.Exe
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.CompletableFuture
import kotlin.time.Clock
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Instant
private class LoggingTest {
@Nested
inner class LoggedProcessTest {
@Test
fun `loggedProcess id should increment with each instantiation`() {
val process1 = process("process1")
val process2 = process("process2")
val process3 = process("process2")
assert(process1.id + 1 == process2.id)
assert(process2.id + 1 == process3.id)
}
@Test
fun `commandString is constructed as expected`() {
val process1 = process("/usr/bin/uv", "install", "requests")
assertEquals("/usr/bin/uv install requests", process1.commandString)
}
@Test
fun `shortenedCommandString is constructed as expected from multiple segments`() {
val process1 = process("/usr/bin/uv", "install", "requests")
assertEquals("uv install requests", process1.shortenedCommandString)
}
@Test
fun `shortenedCommandString is constructed as expected from single segment`() {
val process1 = process("uv", "install", "requests")
assertEquals("uv install requests", process1.shortenedCommandString)
}
}
@TestApplication
@Nested
inner class LoggingProcessTest {
@Test
fun `logged process gets created correctly`() = timeoutRunBlocking(timeout = 1.minutes) {
val traceContext = TraceContext("some trace")
val loggingProcess = fakeLoggingProcess(
stdout = "stdout text",
stderr = "stderr text",
exitValue = 10,
pid = 100,
traceContext = traceContext,
startedAt = Instant.fromEpochSeconds(100),
cwd = "/some/cwd",
pathToExe = "/usr/bin/exe",
args = listOf("foo", "bar"),
env = mapOf("foo" to "bar")
)
val loggedProcess = loggingProcess.loggedProcess
val stdout = loggingProcess.inputStream.readAllBytes().toString(charset = Charsets.UTF_8)
val stderr = loggingProcess.errorStream.readAllBytes().toString(charset = Charsets.UTF_8)
assert(traceContext == loggedProcess.traceContext)
assert(100L == loggedProcess.pid)
assert(Instant.fromEpochSeconds(100) == loggedProcess.startedAt)
assert("/some/cwd" == loggedProcess.cwd)
assert(LoggedProcessExe(path = "/usr/bin/exe", listOf("usr", "bin", "exe")) == loggedProcess.exe)
assert(listOf("foo", "bar") == loggedProcess.args)
assert(mapOf("foo" to "bar") == loggedProcess.env)
assert(stdout == "stdout text")
assert(stderr == "stderr text")
loggingProcess.destroy()
}
@Test
fun `lines get properly collected from out and err`() = timeoutRunBlocking(timeout = 1.minutes) {
val loggingProcess = fakeLoggingProcess(
"outline1\noutline2\noutline3",
"errline1\nerrline2\nerrline3"
)
val loggedProcess = loggingProcess.loggedProcess
assert(loggedProcess.lines.replayCache.isEmpty())
loggingProcess.inputStream.readAllBytes()
loggingProcess.errorStream.readAllBytes()
waitUntil { loggedProcess.lines.replayCache.size == 6 }
(1..3).forEach {
assert(loggedProcess.lines.replayCache[it - 1].text == "outline$it")
assert(loggedProcess.lines.replayCache[it - 1].kind == LoggedProcessLine.Kind.OUT)
}
(4..6).forEach {
assert(loggedProcess.lines.replayCache[it - 1].text == "errline${it - 3}")
assert(loggedProcess.lines.replayCache[it - 1].kind == LoggedProcessLine.Kind.ERR)
}
loggingProcess.destroy()
}
@Test
fun `exit info gets properly populated`() = timeoutRunBlocking(timeout = 1.minutes) {
val now = Clock.System.now()
val loggingProcess = fakeLoggingProcess(
exitValue = 30
)
val loggedProcess = loggingProcess.loggedProcess
loggingProcess.destroy()
waitUntil { loggedProcess.exitInfo.value != null }
assert(loggedProcess.exitInfo.value!!.exitValue == 30)
assert(loggedProcess.exitInfo.value!!.exitedAt >= now)
}
@Disabled
@Test
fun `old lines are evicted when the line limit is reached`() = timeoutRunBlocking(timeout = 1.minutes) {
val loggingProcess = fakeLoggingProcess(
stdout = buildString {
repeat(LoggingLimits.MAX_LINES + 2) {
appendLine("line$it")
}
},
stderr = ""
)
val loggedProcess = loggingProcess.loggedProcess
loggingProcess.inputStream.readAllBytes()
loggingProcess.errorStream.readAllBytes()
loggingProcess.destroy()
waitUntil { loggedProcess.lines.replayCache.last().text == "line${LoggingLimits.MAX_LINES + 1}" }
assert(loggedProcess.lines.replayCache.size == LoggingLimits.MAX_LINES)
assert(loggedProcess.lines.replayCache[0].text == "line2")
}
// todo: add limits test
}
companion object {
fun process(vararg command: String) =
LoggedProcess(
weight = null,
traceContext = null,
pid = 123,
startedAt = Clock.System.now(),
cwd = null,
exe = LoggedProcessExe(
path = command.first(),
parts = command.first().split(Regex("[/\\\\]+"))
),
args = command.drop(1),
env = mapOf(),
target = "Local",
lines = MutableSharedFlow(),
exitInfo = MutableStateFlow(null),
)
fun fakeLoggingProcess(
stdout: String = "stdout",
stderr: String = "stderr",
exitValue: Int = 0,
pid: Long = 0,
traceContext: TraceContext? = null,
startedAt: Instant = Instant.fromEpochSeconds(0),
cwd: String? = "/some/cwd",
pathToExe: String = "/usr/bin/exe",
args: List<String> = listOf("foo", "bar"),
env: Map<String, String> = mapOf("foo" to "bar"),
) =
LoggingProcess(
object : Process() {
val stdoutStream = ByteArrayInputStream(stdout.toByteArray())
val stderrStream = ByteArrayInputStream(stderr.toByteArray())
val stdinStream = ByteArrayOutputStream()
val destroyFuture = CompletableFuture<Int>()
override fun getOutputStream(): OutputStream =
stdinStream
override fun getInputStream(): InputStream =
stdoutStream
override fun getErrorStream(): InputStream =
stderrStream
override fun waitFor(): Int {
destroyFuture.get()
return exitValue
}
override fun exitValue(): Int {
return exitValue
}
override fun destroy() {
destroyFuture.complete(10)
}
override fun pid(): Long =
pid
},
null,
traceContext,
startedAt,
cwd,
Exe.fromString(pathToExe),
args,
env,
"Local",
)
}
}

View File

@@ -39,7 +39,7 @@ jvm_library(
"//python/python-sdk-ui:sdk-ui",
"//python/common",
"//python/python-exec-service/execService.python",
"//python/python-process-output:processOutput",
"//python/python-process-output/common",
]
)
### auto-generated section `build intellij.python.hatch` end

View File

@@ -48,6 +48,6 @@
<orderEntry type="module" module-name="intellij.python.sdk.ui" />
<orderEntry type="module" module-name="intellij.python.common" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
<orderEntry type="module" module-name="intellij.python.processOutput" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
</component>
</module>

View File

@@ -7,7 +7,7 @@
<module name="intellij.python.pyproject"/>
<module name="intellij.python.common"/>
<module name="intellij.python.community.execService.python"/>
<module name="intellij.python.processOutput"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<extensions defaultExtensionNs="Pythonid">
@@ -20,6 +20,6 @@
</extensions>
<extensions defaultExtensionNs="com.intellij">
<python.common.toolToIconMapper implementation="com.intellij.python.hatch.impl.HatchIdMapper"/>
<python.processOutput.processOutputIconMapping implementation="com.intellij.python.hatch.impl.HatchIconMapping"/>
<python.processOutput.common.processOutputIconMapping implementation="com.intellij.python.hatch.impl.HatchIconMapping"/>
</extensions>
</idea-plugin>

View File

@@ -1,9 +1,9 @@
package com.intellij.python.hatch.impl
import com.intellij.python.hatch.icons.PythonHatchIcons
import com.intellij.python.processOutput.ProcessBinaryFileName
import com.intellij.python.processOutput.ProcessIcon
import com.intellij.python.processOutput.ProcessOutputIconMapping
import com.intellij.python.processOutput.common.ProcessBinaryFileName
import com.intellij.python.processOutput.common.ProcessIcon
import com.intellij.python.processOutput.common.ProcessOutputIconMapping
internal class HatchIconMapping : ProcessOutputIconMapping() {
override val mapping: Map<ProcessBinaryFileName, ProcessIcon> =

View File

@@ -20,7 +20,7 @@ jvm_library(
"//platform/util",
"//python/common",
"//python/openapi:community",
"//python/python-process-output:processOutput",
"//python/python-process-output/common",
]
)
### auto-generated section `build intellij.python.community.impl.poetry.common` end

View File

@@ -15,6 +15,6 @@
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.python.common" />
<orderEntry type="module" module-name="intellij.python.community" />
<orderEntry type="module" module-name="intellij.python.processOutput" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
</component>
</module>

View File

@@ -2,10 +2,10 @@
<dependencies>
<module name="intellij.python.common"/>
<module name="intellij.python.community"/>
<module name="intellij.python.processOutput"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<python.common.toolToIconMapper implementation="com.intellij.python.community.impl.poetry.common.impl.PoetryToolIdMapper"/>
<python.processOutput.processOutputIconMapping implementation="com.intellij.python.community.impl.poetry.common.impl.PoetryIconMapping"/>
<python.processOutput.common.processOutputIconMapping implementation="com.intellij.python.community.impl.poetry.common.impl.PoetryIconMapping"/>
</extensions>
</idea-plugin>

View File

@@ -2,9 +2,9 @@
package com.intellij.python.community.impl.poetry.common.impl
import com.intellij.python.community.impl.poetry.common.icons.PythonCommunityImplPoetryCommonIcons
import com.intellij.python.processOutput.ProcessBinaryFileName
import com.intellij.python.processOutput.ProcessIcon
import com.intellij.python.processOutput.ProcessOutputIconMapping
import com.intellij.python.processOutput.common.ProcessBinaryFileName
import com.intellij.python.processOutput.common.ProcessIcon
import com.intellij.python.processOutput.common.ProcessOutputIconMapping
internal class PoetryIconMapping : ProcessOutputIconMapping() {
override val mapping: Map<ProcessBinaryFileName, ProcessIcon> =

View File

@@ -1,22 +0,0 @@
### auto-generated section `build intellij.python.processOutput` start
load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
resourcegroup(
name = "processOutput_resources",
srcs = glob(["resources/**/*"]),
strip_prefix = "resources"
)
jvm_library(
name = "processOutput",
module_name = "intellij.python.processOutput",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = [":processOutput_resources"],
deps = [
"@lib//:kotlin-stdlib",
"//platform/platform-api:ide",
"//platform/core-api:core",
]
)
### auto-generated section `build intellij.python.processOutput` end

View File

@@ -0,0 +1,23 @@
### auto-generated section `build intellij.python.processOutput.backend` start
load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
resourcegroup(
name = "backend_resources",
srcs = glob(["resources/**/*"]),
strip_prefix = "resources"
)
jvm_library(
name = "backend",
module_name = "intellij.python.processOutput.backend",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = [":backend_resources"],
deps = [
"//platform/backend",
"@lib//:kotlin-stdlib",
"//platform/kernel/rpc.backend",
"//python/python-process-output/common",
]
)
### auto-generated section `build intellij.python.processOutput.backend` end

View File

@@ -4,12 +4,13 @@
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.python.processOutput.backend" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module" module-name="intellij.platform.backend" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.rpc.backend" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
</component>
</module>

View File

@@ -0,0 +1,12 @@
<idea-plugin visibility="internal">
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<module name="intellij.platform.backend"/>
<module name="intellij.platform.rpc.backend"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<!-- endregion -->
<extensions defaultExtensionNs="com.intellij.platform">
<rpc.backend.remoteApiProvider implementation="com.intellij.python.processOutput.backend.ApiProvider"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,25 @@
package com.intellij.python.processOutput.backend
import com.intellij.platform.rpc.backend.RemoteApiProvider
import com.intellij.python.processOutput.common.BackEndApi
import com.intellij.python.processOutput.common.QueryId
import com.intellij.python.processOutput.common.QueryResponsePayload
import com.intellij.python.processOutput.common.resolveProcessOutputQuery
import fleet.rpc.remoteApiDescriptor
internal class ApiProvider : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<BackEndApi>()) {
BackendApiImpl
}
}
}
private object BackendApiImpl : BackEndApi {
override suspend fun sendProcessOutputQueryResponse(
requestId: QueryId,
response: QueryResponsePayload,
) {
resolveProcessOutputQuery(requestId, response)
}
}

View File

@@ -0,0 +1,41 @@
### auto-generated section `build intellij.python.processOutput.common` start
load("//build:compiler-options.bzl", "create_kotlinc_options")
load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
create_kotlinc_options(
name = "custom_common",
x_context_parameters = True
)
resourcegroup(
name = "common_resources",
srcs = glob(["resources/**/*"]),
strip_prefix = "resources"
)
jvm_library(
name = "common",
module_name = "intellij.python.processOutput.common",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = [":common_resources"],
kotlinc_opts = ":custom_common",
deps = [
"//platform/kernel/rpc",
"@lib//:kotlin-stdlib",
"//platform/core-api:core",
"//platform/projectModel-api:projectModel",
"//platform/platform-impl:ide-impl",
"//platform/kernel/shared:kernel",
"//libraries/kotlinx/coroutines/core",
"//libraries/kotlinx/serialization/core",
"//libraries/kotlinx/serialization/json",
"//platform/platform-impl/rpc",
"//platform/execution",
"//platform/execution-impl",
"//platform/util/coroutines",
"//platform/project/shared:project",
"//platform/remote-topics/shared:rpc-topics",
]
)
### auto-generated section `build intellij.python.processOutput.common` end

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="kotlin-language" name="Kotlin">
<configuration version="5" platform="JVM 21" allPlatforms="JVM [21]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xjvm-default=all -XXLanguage:+AllowEagerSupertypeAccessibilityChecks -Xcontext-parameters -progressive" />
</compilerSettings>
<compilerArguments>
<stringArguments>
<stringArg name="jvmTarget" arg="21" />
<stringArg name="apiVersion" arg="2.3" />
<stringArg name="languageVersion" arg="2.3" />
</stringArguments>
<arrayArguments>
<arrayArg name="pluginClasspaths">
<args>
<arg>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</arg>
<arg>$MAVEN_REPOSITORY$/com/jetbrains/fleet/rpc-compiler-plugin/2.3.10-RC-0.1/rpc-compiler-plugin-2.3.10-RC-0.1.jar</arg>
</args>
</arrayArg>
<arrayArg name="pluginOptions" />
</arrayArguments>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.python.processOutput.common" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module" module-name="intellij.platform.rpc" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.kernel" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.core" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.json" />
<orderEntry type="module" module-name="intellij.platform.ide.rpc" />
<orderEntry type="module" module-name="intellij.platform.execution" />
<orderEntry type="module" module-name="intellij.platform.execution.impl" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
<orderEntry type="module" module-name="intellij.platform.project" />
<orderEntry type="module" module-name="intellij.platform.rpc.topics" />
</component>
</module>

View File

@@ -0,0 +1,11 @@
<idea-plugin visibility="internal">
<extensionPoints>
<extensionPoint
qualifiedName="com.intellij.python.processOutput.common.processOutputIconMapping"
interface="com.intellij.python.processOutput.common.ProcessOutputIconMapping"
/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<platform.rpc.applicationRemoteTopicListener implementation="com.intellij.python.processOutput.common.ProcessOutputTopicListener"/>
</extensions>
</idea-plugin>

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput
package com.intellij.python.processOutput.common
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon

View File

@@ -0,0 +1,131 @@
package com.intellij.python.processOutput.common
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.platform.rpc.RemoteApiProviderService
import fleet.rpc.RemoteApi
import fleet.rpc.Rpc
import fleet.rpc.remoteApiDescriptor
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.util.concurrent.atomic.AtomicLong
import kotlin.collections.set
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
private val QUERY_RESPONSE_TIMEOUT: Duration = 10.seconds
@ApiStatus.Internal
@Serializable
sealed interface QueryResponsePayload {
@Serializable
data object UnitPayload : QueryResponsePayload
@Serializable
data class BooleanPayload(val value: Boolean) : QueryResponsePayload
}
@ApiStatus.Internal
@Serializable
sealed interface QueryResponse<QueryResponsePayload> {
@Serializable
class Timeout<TResponse : QueryResponsePayload> : QueryResponse<TResponse>
@Serializable
data class Completed<TResponse : QueryResponsePayload>(val payload: TResponse) : QueryResponse<TResponse>
}
@ApiStatus.Internal
@Serializable
class QueryId internal constructor() {
internal val id: Long = atomicId.getAndAdd(1)
override fun equals(other: Any?): Boolean =
when {
this === other -> true
other !is QueryId -> false
else -> id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
companion object {
private val atomicId = AtomicLong(0)
}
}
@ApiStatus.Internal
@Serializable
sealed class ProcessOutputQuery<TResponse : QueryResponsePayload> {
internal val id: QueryId = QueryId()
suspend fun respond(response: TResponse) {
BackEndApi().sendProcessOutputQueryResponse(id, response)
}
@Serializable
data class SpecifyAdditionalMessageToUser(
val processId: Int,
val messageToUser: @Nls String,
) : ProcessOutputQuery<QueryResponsePayload.UnitPayload>()
@Serializable
data class OpenToolWindowWithError(
val processId: Int,
) : ProcessOutputQuery<QueryResponsePayload.BooleanPayload>()
}
@ApiStatus.Internal
suspend fun <TResponse : QueryResponsePayload> sendProcessOutputQuery(
query: ProcessOutputQuery<TResponse>,
): QueryResponse<TResponse> {
val deferred = CompletableDeferred<TResponse>()
val service = ApplicationManager.getApplication().service<QueryService>()
@Suppress("UNCHECKED_CAST")
service.queries[query.id] = deferred as CompletableDeferred<Any>
sendProcessOutputTopicEvent(ProcessOutputEventDto.ReceivedQuery(query))
val value =
try {
withTimeout(QUERY_RESPONSE_TIMEOUT) {
QueryResponse.Completed(deferred.await())
}
}
catch (_: TimeoutCancellationException) {
QueryResponse.Timeout()
}
finally {
service.queries.remove(query.id)
}
return value
}
@Service
private class QueryService {
val queries = mutableMapOf<QueryId, CompletableDeferred<Any>>()
}
@ApiStatus.Internal
fun resolveProcessOutputQuery(requestId: QueryId, payload: QueryResponsePayload) {
val service = ApplicationManager.getApplication().service<QueryService>()
service.queries[requestId]?.complete(payload)
}
@ApiStatus.Internal
@Rpc
interface BackEndApi : RemoteApi<Unit> {
suspend fun sendProcessOutputQueryResponse(requestId: QueryId, response: QueryResponsePayload)
}
@ApiStatus.Internal
suspend fun BackEndApi(): BackEndApi = RemoteApiProviderService.resolve(remoteApiDescriptor<BackEndApi>())

View File

@@ -0,0 +1,131 @@
package com.intellij.python.processOutput.common
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.platform.rpc.topics.ApplicationRemoteTopic
import com.intellij.platform.rpc.topics.ApplicationRemoteTopicListener
import com.intellij.platform.rpc.topics.sendToClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import kotlin.time.Instant
@ApiStatus.Internal
@Serializable
enum class TraceContextKind {
NON_INTERACTIVE,
INTERACTIVE,
}
@ApiStatus.Internal
@Serializable
@JvmInline
value class TraceContextUuid(val uuid: String)
@ApiStatus.Internal
@Serializable
data class TraceContextDto(
val title: @Nls String,
val timestamp: Long,
val uuid: TraceContextUuid,
val kind: TraceContextKind,
val parentUuid: TraceContextUuid?,
)
@ApiStatus.Internal
@Serializable
data class ExecutableDto(
val path: String,
val parts: List<String>,
)
@ApiStatus.Internal
@Serializable
enum class ProcessWeightDto {
LIGHT,
MEDIUM,
HEAVY
}
@ApiStatus.Internal
@Serializable
data class LoggedProcessDto(
val weight: ProcessWeightDto?,
val traceContextUuid: TraceContextUuid?,
val pid: Long?,
val startedAt: Instant,
val cwd: String?,
val exe: ExecutableDto,
val args: List<String>,
val env: Map<String, String>,
val target: String,
val id: Int,
)
@ApiStatus.Internal
@Serializable
enum class OutputKindDto {
OUT,
ERR,
}
@ApiStatus.Internal
@Serializable
data class OutputLineDto(
val kind: OutputKindDto,
val text: String,
val lineNo: Int,
)
@ApiStatus.Internal
@Serializable
sealed interface ProcessOutputEventDto {
@Serializable
data class NewProcess(val loggedProcess: LoggedProcessDto, val traceHierarchy: List<TraceContextDto>) : ProcessOutputEventDto
@Serializable
data class NewOutputLine(val processId: Int, val outputLine: OutputLineDto) : ProcessOutputEventDto
@Serializable
data class ProcessExit(val processId: Int, val exitedAt: Instant, val exitValue: Int) : ProcessOutputEventDto
@Serializable
class ReceivedQuery<TResponse : QueryResponsePayload> internal constructor(
val query: ProcessOutputQuery<TResponse>,
) : ProcessOutputEventDto
}
@ApiStatus.Internal
val PROCESS_OUTPUT_TOPIC: ApplicationRemoteTopic<ProcessOutputEventDto> =
ApplicationRemoteTopic("PythonProcessOutputTopic", ProcessOutputEventDto.serializer())
@ApiStatus.Internal
fun sendProcessOutputTopicEvent(event: ProcessOutputEventDto) {
PROCESS_OUTPUT_TOPIC.sendToClient(event)
}
internal class ProcessOutputTopicListener : ApplicationRemoteTopicListener<ProcessOutputEventDto> {
override val topic: ApplicationRemoteTopic<ProcessOutputEventDto> = PROCESS_OUTPUT_TOPIC
override fun handleEvent(event: ProcessOutputEventDto) {
val service = ApplicationManager.getApplication().service<FrontendTopicService>()
service.coroutineScope.launch {
eventsInternal.emit(event)
}
}
}
private val eventsInternal = MutableSharedFlow<ProcessOutputEventDto>()
@ApiStatus.Internal
@Service
class FrontendTopicService(internal val coroutineScope: CoroutineScope) {
val events: SharedFlow<ProcessOutputEventDto> = eventsInternal.asSharedFlow()
}

View File

@@ -1,33 +1,35 @@
### auto-generated section `build intellij.python.processOutput.impl` start
### auto-generated section `build intellij.python.processOutput.frontend` start
load("//build:compiler-options.bzl", "create_kotlinc_options")
load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
create_kotlinc_options(
name = "custom_impl",
name = "custom_frontend",
opt_in = ["kotlin.time.ExperimentalTime"],
plugin_options = ["plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaAnnotations=true"]
plugin_options = ["plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaAnnotations=true"],
x_context_parameters = True
)
resourcegroup(
name = "impl_resources",
name = "frontend_resources",
srcs = glob(["resources/**/*"]),
strip_prefix = "resources"
)
resourcegroup(
name = "impl_test_resources",
name = "frontend_test_resources",
srcs = glob(["testResources/**/*"]),
strip_prefix = "testResources"
)
jvm_library(
name = "impl",
module_name = "intellij.python.processOutput.impl",
name = "frontend",
module_name = "intellij.python.processOutput.frontend",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = [":impl_resources"],
kotlinc_opts = ":custom_impl",
resources = [":frontend_resources"],
kotlinc_opts = ":custom_frontend",
deps = [
"//platform/platform-frontend:frontend",
"@lib//:kotlin-stdlib",
"//platform/platform-api:ide",
"//platform/core-api:core",
@@ -37,30 +39,27 @@ jvm_library(
"//platform/jewel/ui",
"//platform/jewel/ide-laf-bridge",
"//platform/core-ui",
"//python/python-exec-service:community-execService",
"//python/openapi:community",
"//platform/util:util-ui",
"//platform/editor-ui-api:editor-ui",
"//platform/projectModel-api:projectModel",
"//platform/execution-impl",
"//libraries/jediterm-core",
"//libraries/compose-runtime-desktop",
"//platform/statistics",
"//python/python-process-output:processOutput",
"//libraries/kotlinx/collections-immutable:libraries-kotlinx-collections-immutable",
"//python/python-process-output/common",
"//libraries/kotlin/reflect",
],
plugins = ["@lib//:compose-plugin"]
)
jvm_library(
name = "impl_test_lib",
name = "frontend_test_lib",
visibility = ["//visibility:public"],
srcs = glob(["test/**/*.kt", "test/**/*.java", "test/**/*.form"], allow_empty = True),
resources = [":impl_test_resources"],
kotlinc_opts = ":custom_impl",
associates = [":impl"],
resources = [":frontend_test_resources"],
kotlinc_opts = ":custom_frontend",
associates = [":frontend"],
deps = [
"//platform/platform-frontend:frontend",
"@lib//:kotlin-stdlib",
"//platform/platform-api:ide",
"//platform/core-api:core",
@@ -85,7 +84,6 @@ jvm_library(
"//platform/jewel/int-ui/int-ui-standalone:jewel-intUi-standalone",
"//platform/projectModel-api:projectModel",
"//platform/execution-impl",
"//libraries/jediterm-core",
"//libraries/compose-runtime-desktop",
"@lib//:io-mockk",
"@lib//:io-mockk-jvm",
@@ -94,26 +92,25 @@ jvm_library(
"//platform/testFramework/common",
"//platform/statistics",
"//platform/statistics:statistics_test_lib",
"//python/python-process-output:processOutput",
"//python/python-process-output/common",
"//python/junit5Tests-framework:community-junit5Tests-framework_test_lib",
"//libraries/compose-foundation-desktop-junit",
"//libraries/kotlinx/coroutines/debug",
"//libraries/junit5",
"//libraries/junit4",
"//libraries/kotlinx/collections-immutable:libraries-kotlinx-collections-immutable",
"//libraries/kotlinx/coroutines/test",
"//libraries/kotlin/reflect",
"//python/python-test-env/junit5:junit5_test_lib",
],
plugins = ["@lib//:compose-plugin"]
)
### auto-generated section `build intellij.python.processOutput.impl` end
### auto-generated section `build intellij.python.processOutput.frontend` end
### auto-generated section `test intellij.python.processOutput.impl` start
### auto-generated section `test intellij.python.processOutput.frontend` start
load("@community//build:tests-options.bzl", "jps_test")
jps_test(
name = "impl_test",
runtime_deps = [":impl_test_lib"]
name = "frontend_test",
runtime_deps = [":frontend_test_lib"]
)
### auto-generated section `test intellij.python.processOutput.impl` end
### auto-generated section `test intellij.python.processOutput.frontend` end

View File

@@ -4,7 +4,7 @@
<facet type="kotlin-language" name="Kotlin">
<configuration version="5" platform="JVM 21" allPlatforms="JVM [21]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xjvm-default=all -P plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaAnnotations=true -XXLanguage:+AllowEagerSupertypeAccessibilityChecks -opt-in=kotlin.time.ExperimentalTime -progressive" />
<option name="additionalArguments" value="-Xjvm-default=all -P plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaAnnotations=true -XXLanguage:+AllowEagerSupertypeAccessibilityChecks -opt-in=kotlin.time.ExperimentalTime -Xcontext-parameters -progressive" />
</compilerSettings>
<compilerArguments>
<stringArguments>
@@ -16,6 +16,7 @@
<arrayArg name="pluginClasspaths">
<args>$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.3.10-RC/kotlin-compose-compiler-plugin-2.3.10-RC.jar</args>
</arrayArg>
<arrayArg name="pluginOptions" />
</arrayArguments>
</compilerArguments>
</configuration>
@@ -25,11 +26,12 @@
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.python.processOutput.frontend" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" packagePrefix="com.intellij.python" />
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module" module-name="intellij.platform.frontend" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide" />
@@ -40,8 +42,8 @@
<orderEntry type="module" module-name="intellij.platform.jewel.ui" />
<orderEntry type="module" module-name="intellij.platform.jewel.ideLafBridge" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.python.community.execService" />
<orderEntry type="module" module-name="intellij.python.community" />
<orderEntry type="module" module-name="intellij.python.community.execService" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.util.ui" />
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
<orderEntry type="module" module-name="intellij.libraries.kotlinTest" scope="TEST" />
@@ -50,20 +52,18 @@
<orderEntry type="module" module-name="intellij.platform.jewel.intUi.standalone" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.execution.impl" />
<orderEntry type="module" module-name="intellij.libraries.jediterm.core" />
<orderEntry type="module" module-name="intellij.libraries.compose.runtime.desktop" />
<orderEntry type="library" scope="TEST" name="io.mockk" level="project" />
<orderEntry type="library" scope="TEST" name="io.mockk.jvm" level="project" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework.common" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.python.processOutput" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.compose.foundation.desktop.junit" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.debug" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.junit4" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.collections.immutable" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.test" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.kotlin.reflect" />
<orderEntry type="module" module-name="intellij.python.test.env.junit5" scope="TEST" />

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,26 +1,20 @@
<idea-plugin visibility="internal">
<dependencies>
<module name="intellij.python.community"/>
<module name="intellij.python.sdk"/>
<module name="intellij.python.processOutput"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.platform.jewel.foundation"/>
<module name="intellij.platform.jewel.ui"/>
<module name="intellij.platform.jewel.ideLafBridge"/>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.platform.frontend"/>
<module name="intellij.libraries.compose.foundation.desktop"/>
<module name="intellij.libraries.compose.runtime.desktop"/>
<module name="intellij.libraries.skiko"/>
<!-- endregion -->
<module name="intellij.platform.jewel.foundation"/>
<module name="intellij.platform.jewel.ideLafBridge"/>
<module name="intellij.platform.jewel.ui"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<toolWindow id="PythonProcessOutput"
anchor="bottom"
secondary="false"
icon="com.intellij.python.processOutput.impl.icons.PythonProcessOutputImplIcons.Process"
factoryClass="com.intellij.python.processOutput.impl.ProcessOutputToolWindowFactory"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.python.processOutput.impl.ProcessOutputUsageCollector"/>
<python.processOutput.processOutputApi implementation="com.intellij.python.processOutput.impl.ProcessOutputApiImpl"/>
icon="com.intellij.python.processOutput.frontend.icons.PythonProcessOutputFrontendIcons.Process"
factoryClass="com.intellij.python.processOutput.frontend.ProcessOutputToolWindowFactory"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.python.processOutput.frontend.ProcessOutputUsageCollector"/>
</extensions>
</idea-plugin>
</idea-plugin>

View File

@@ -43,5 +43,6 @@ process.output.output.sections.output=Process Output
process.output.output.blankMessage=Select a process to view its output
process.output.filters.tree.time=Show start time
process.output.filters.tree.processWeight=Show process weights
process.output.filters.tree.backgroundProcesses=Show background processes
process.output.filters.output.tags=Show tags

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl
package com.intellij.python.processOutput.frontend
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls

View File

@@ -0,0 +1,755 @@
package com.intellij.python.processOutput.frontend
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.util.fastMaxOfOrDefault
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.python.processOutput.common.FrontendTopicService
import com.intellij.python.processOutput.common.LoggedProcessDto
import com.intellij.python.processOutput.common.OutputKindDto
import com.intellij.python.processOutput.common.OutputLineDto
import com.intellij.python.processOutput.common.ProcessBinaryFileName
import com.intellij.python.processOutput.common.ProcessIcon
import com.intellij.python.processOutput.common.ProcessOutputEventDto
import com.intellij.python.processOutput.common.ProcessOutputQuery
import com.intellij.python.processOutput.common.QueryResponsePayload
import com.intellij.python.processOutput.common.TraceContextDto
import com.intellij.python.processOutput.common.TraceContextKind
import com.intellij.python.processOutput.common.TraceContextUuid
import com.intellij.python.processOutput.frontend.ProcessOutputBundle.message
import com.intellij.python.processOutput.frontend.ui.components.Filter
import com.intellij.python.processOutput.frontend.ui.components.FilterActionGroupState
import com.intellij.python.processOutput.frontend.ui.components.FilterItem
import com.intellij.python.processOutput.frontend.ui.components.OutputSectionTestTags
import com.intellij.python.processOutput.frontend.ui.components.TreeSectionTestTags
import com.intellij.python.processOutput.frontend.ui.shortenedCommandString
import java.util.WeakHashMap
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Instant
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.jewel.foundation.lazy.SelectableLazyListState
import org.jetbrains.jewel.foundation.lazy.tree.Tree
import org.jetbrains.jewel.foundation.lazy.tree.TreeGeneratorScope
import org.jetbrains.jewel.foundation.lazy.tree.TreeState
import org.jetbrains.jewel.foundation.lazy.tree.buildTree
@ApiStatus.Internal
object CoroutineNames {
const val EXIT_INFO_COLLECTOR: String = "ProcessOutput.ExitInfoCollector"
}
internal object ProcessOutputControllerServiceLimits {
const val MAX_PROCESSES = 512
const val MAX_LINES = 1024
}
@ApiStatus.Internal
sealed interface ProcessStatus {
data object Running : ProcessStatus
data class Done(
val exitedAt: Instant,
val exitCode: Int,
val additionalMessageToUser: @Nls String? = null,
val isCritical: Boolean = false,
) : ProcessStatus
}
@ApiStatus.Internal
data class LoggedProcess(
val data: LoggedProcessDto,
val lines: List<OutputLineDto>,
val status: StateFlow<ProcessStatus>,
)
internal interface ProcessOutputController {
val selectedProcess: StateFlow<LoggedProcess?>
val processTreeUiState: TreeUiState
val processOutputUiState: OutputUiState
fun resolveTraceContext(uuid: TraceContextUuid): TraceContextDto?
fun collapseAllContexts()
fun expandAllContexts()
fun selectProcess(process: LoggedProcess?)
fun onTreeFilterItemToggled(filterItem: TreeFilter.Item, enabled: Boolean)
fun onOutputFilterItemToggled(filterItem: OutputFilter.Item, enabled: Boolean)
fun toggleProcessInfo()
fun toggleProcessOutput()
fun copyOutputToClipboard(loggedProcess: LoggedProcess)
fun copyOutputTagAtIndexToClipboard(loggedProcess: LoggedProcess, fromIndex: Int)
fun copyOutputExitInfoToClipboard(loggedProcess: LoggedProcess)
}
@ApiStatus.Internal
data class TreeUiState(
val filters: FilterActionGroupState<TreeFilter, TreeFilter.Item>,
val searchState: TextFieldState,
val selectableLazyListState: SelectableLazyListState,
val treeState: TreeState,
val tree: StateFlow<Tree<TreeNode>>,
)
@ApiStatus.Internal
object TreeFilter : Filter<TreeFilter.Item> {
enum class Item(override val title: String, override val testTag: String) : FilterItem {
SHOW_TIME(
title = message("process.output.filters.tree.time"),
testTag = TreeSectionTestTags.FILTERS_TIME,
),
SHOW_PROCESS_WEIGHT(
title = message("process.output.filters.tree.processWeight"),
testTag = TreeSectionTestTags.FILTERS_PROCESS_WEIGHTS,
),
SHOW_BACKGROUND_PROCESSES(
title = message("process.output.filters.tree.backgroundProcesses"),
testTag = TreeSectionTestTags.FILTERS_BACKGROUND,
),
}
override val defaultActive: Set<Item> = setOf(Item.SHOW_TIME, Item.SHOW_PROCESS_WEIGHT)
}
@ApiStatus.Internal
sealed interface TreeNode {
data class Context(
val traceContext: TraceContextDto,
) : TreeNode
data class Process(
val process: LoggedProcess,
val icon: ProcessIcon?,
) : TreeNode
}
@ApiStatus.Internal
object OutputFilter : Filter<OutputFilter.Item> {
enum class Item(override val title: String, override val testTag: String) : FilterItem {
SHOW_TAGS(
title = message("process.output.filters.output.tags"),
testTag = OutputSectionTestTags.FILTERS_TAGS,
);
}
override val defaultActive: Set<Item> = setOf(Item.SHOW_TAGS)
}
@ApiStatus.Internal
data class OutputUiState(
val filters: FilterActionGroupState<OutputFilter, OutputFilter.Item>,
val isInfoExpanded: StateFlow<Boolean>,
val isOutputExpanded: StateFlow<Boolean>,
val lazyListState: LazyListState,
)
internal class InternalLoggedProcess(
val data: LoggedProcessDto,
val lines: SnapshotStateList<OutputLineDto>,
val status: MutableStateFlow<ProcessStatus>,
)
@Service(Service.Level.PROJECT)
internal class ProcessOutputControllerService(
private val project: Project,
private val coroutineScope: CoroutineScope,
) : ProcessOutputController {
private val shouldScrollToTop = MutableStateFlow(false)
internal val loggedProcesses = MutableStateFlow<List<LoggedProcess>>(listOf())
private val processTree = MutableStateFlow(value = buildTree<TreeNode> {})
private val processOutputInfoExpanded = MutableStateFlow(false)
private val processOutputOutputExpanded = MutableStateFlow(true)
override val selectedProcess: MutableStateFlow<LoggedProcess?> = MutableStateFlow(null)
override val processTreeUiState: TreeUiState = run {
val selectableLazyListState = SelectableLazyListState(LazyListState())
TreeUiState(
filters = FilterActionGroupState(TreeFilter),
searchState = TextFieldState(),
selectableLazyListState = selectableLazyListState,
treeState = TreeState(selectableLazyListState),
tree = processTree,
)
}
override val processOutputUiState: OutputUiState = OutputUiState(
filters = FilterActionGroupState(OutputFilter),
isInfoExpanded = processOutputInfoExpanded,
isOutputExpanded = processOutputOutputExpanded,
lazyListState = LazyListState(),
)
private val traceContextCache = boundedLinkedHashMap<TraceContextUuid, TraceContextDto>(
ProcessOutputControllerServiceLimits.MAX_PROCESSES * 2,
)
private val iconMapping = ProcessOutputIconMappingData.mapping
private val iconMatchers = ProcessOutputIconMappingData.matchers
private val iconCache = WeakHashMap<LoggedProcess, ProcessIcon>()
init {
collectTopicEvents()
collectSearchStats()
collectProcessTree()
ensureProcessTreeScroll()
}
override fun resolveTraceContext(uuid: TraceContextUuid): TraceContextDto? =
traceContextCache[uuid]
override fun collapseAllContexts() {
processTreeUiState.treeState.openNodes = setOf()
ProcessOutputUsageCollector.treeCollapseAllClicked()
}
override fun expandAllContexts() {
processTreeUiState.treeState.openNodes =
processTreeUiState.tree.value
.walkDepthFirst()
.mapNotNull {
when (val data = it.data) {
is TreeNode.Context -> data.traceContext
is TreeNode.Process -> null
}
}
.toSet()
ProcessOutputUsageCollector.treeExpandAllClicked()
}
override fun selectProcess(process: LoggedProcess?) {
selectedProcess.value = process
ProcessOutputUsageCollector.treeProcessSelected()
}
override fun onTreeFilterItemToggled(filterItem: TreeFilter.Item, enabled: Boolean) {
when (filterItem) {
TreeFilter.Item.SHOW_BACKGROUND_PROCESSES ->
coroutineScope.launch(Dispatchers.EDT) {
processTreeUiState.selectableLazyListState.lazyListState.scrollToItem(0)
}
TreeFilter.Item.SHOW_TIME, TreeFilter.Item.SHOW_PROCESS_WEIGHT -> {}
}
ProcessOutputUsageCollector.treeFilterToggled(filterItem, enabled)
}
override fun onOutputFilterItemToggled(filterItem: OutputFilter.Item, enabled: Boolean) {
ProcessOutputUsageCollector.outputFilterToggled(filterItem, enabled)
}
override fun toggleProcessInfo() {
val expanded = processOutputInfoExpanded.value
processOutputInfoExpanded.value = !expanded
ProcessOutputUsageCollector.outputProcessInfoToggled(!expanded)
}
override fun toggleProcessOutput() {
val expanded = processOutputOutputExpanded.value
processOutputOutputExpanded.value = !expanded
ProcessOutputUsageCollector.outputProcessOutputToggled(!expanded)
}
override fun copyOutputToClipboard(loggedProcess: LoggedProcess) {
val showTags = processOutputUiState.filters.active.contains(OutputFilter.Item.SHOW_TAGS)
val stringToCopy = buildString {
loggedProcess.lines.forEach { line ->
if (showTags) {
val tag = when (line.kind) {
OutputKindDto.OUT -> Tag.OUTPUT
OutputKindDto.ERR -> Tag.ERROR
}
append("[$tag] ".padStart(Tag.maxLength + 3))
} else {
repeat(Tag.maxLength + 3) {
append(' ')
}
}
appendLine(line.text)
}
val exitData = when (val status = loggedProcess.status.value) {
ProcessStatus.Running -> null
is ProcessStatus.Done -> status
}
exitData?.also { exitData ->
append("[${Tag.EXIT}] ".padStart(Tag.maxLength + 3))
append(exitData.exitCode)
exitData.additionalMessageToUser?.also { message ->
append(": ")
append(message)
}
appendLine()
}
}
CopyPasteManager.copyTextToClipboard(stringToCopy)
ProcessOutputUsageCollector.outputCopyClicked()
}
override fun copyOutputTagAtIndexToClipboard(
loggedProcess: LoggedProcess,
fromIndex: Int,
) {
val stringToCopy = buildString {
val lines = loggedProcess.lines
lines
.drop(fromIndex)
.takeWhile { it.kind == lines[fromIndex].kind }
.forEach {
appendLine(it.text)
}
}
CopyPasteManager.copyTextToClipboard(stringToCopy)
ProcessOutputUsageCollector.outputTagSectionCopyClicked()
}
override fun copyOutputExitInfoToClipboard(loggedProcess: LoggedProcess) {
val exitData = when (val status = loggedProcess.status.value) {
ProcessStatus.Running -> return
is ProcessStatus.Done -> status
}
val stringToCopy = buildString {
append(exitData.exitCode)
exitData.additionalMessageToUser?.also { message ->
append(": ")
append(message)
}
appendLine()
}
CopyPasteManager.copyTextToClipboard(stringToCopy)
ProcessOutputUsageCollector.outputExitInfoCopyClicked()
}
private fun tryOpenLogInToolWindow(logId: Int): Boolean {
val process = loggedProcesses.value.find { process -> process.data.id == logId }
?: return false
coroutineScope.launch(Dispatchers.EDT) {
ToolWindowManager.getInstance(project).getToolWindow(TOOL_WINDOW_ID)?.show()
// select the process
selectedProcess.value = process
// open all the parent nodes of the process
process.data.traceContextUuid
?.let { resolveTraceContext(it) }
?.also {
processTreeUiState.treeState.openNodes(it.hierarchy())
}
// select the process in the list state
processTreeUiState.treeState.selectedKeys = setOf(process)
// scroll to the top of the list
processTreeUiState.selectableLazyListState.lazyListState.scrollToItem(0)
// clear search text
processTreeUiState.searchState.clearText()
// expand process output section
processOutputOutputExpanded.value = true
// wait until output has recomposed
delay(100.milliseconds)
// scroll output all the way to the bottom
val index = processOutputUiState.lazyListState.layoutInfo.totalItemsCount
processOutputUiState.lazyListState.scrollToItem(index.coerceAtLeast(0))
}
ProcessOutputUsageCollector.toolwindowOpenedDueToError()
return true
}
private fun collectTopicEvents() {
val processMap = boundedLinkedHashMap<Int, InternalLoggedProcess>(
ProcessOutputControllerServiceLimits.MAX_PROCESSES,
)
var processList = listOf<LoggedProcess>()
coroutineScope.launch {
val service = ApplicationManager.getApplication().service<FrontendTopicService>()
service.events.collect { event ->
when (event) {
is ProcessOutputEventDto.NewProcess -> {
// If a new item was added while the process tree is fully scrolled to top,
// we need to manually scroll all the way to top once a new item is added to
// the list state, as it is not done automatically.
if (!processTreeUiState.treeState.canScrollBackward) {
shouldScrollToTop.value = true
}
for (traceContext in event.traceHierarchy) {
if (traceContext.uuid !in traceContextCache) {
traceContextCache[traceContext.uuid] = traceContext
}
}
val internalProcess = InternalLoggedProcess(
data = event.loggedProcess,
lines = SnapshotStateList(),
status = MutableStateFlow(ProcessStatus.Running),
)
processMap[event.loggedProcess.id] = internalProcess
processList = processList + LoggedProcess(
data = internalProcess.data,
lines = internalProcess.lines,
status = internalProcess.status,
)
if (processList.size > ProcessOutputControllerServiceLimits.MAX_PROCESSES) {
processList = processList.drop(
processList.size -
ProcessOutputControllerServiceLimits.MAX_PROCESSES,
)
}
event.loggedProcess.traceContextUuid
?.let { resolveTraceContext(it) }
?.takeIf { context ->
context.kind != TraceContextKind.NON_INTERACTIVE
}
?.also { context ->
processTreeUiState.treeState.openNodes(context.hierarchy())
}
loggedProcesses.emit(processList)
}
is ProcessOutputEventDto.NewOutputLine -> {
val internalProcess = processMap[event.processId]
if (internalProcess != null) {
Snapshot.withMutableSnapshot {
val lines = internalProcess.lines
lines += event.outputLine
lines.sortBy { it.lineNo }
if (lines.size > ProcessOutputControllerServiceLimits.MAX_LINES) {
lines.drop(
lines.size - ProcessOutputControllerServiceLimits.MAX_LINES,
)
}
}
}
}
is ProcessOutputEventDto.ProcessExit -> {
val internalProcess = processMap[event.processId]
internalProcess?.status?.emit(
ProcessStatus.Done(
exitedAt = event.exitedAt,
exitCode = event.exitValue,
),
)
}
is ProcessOutputEventDto.ReceivedQuery<*> ->
when (val query = event.query) {
is ProcessOutputQuery.OpenToolWindowWithError -> {
val hasOpened = tryOpenLogInToolWindow(query.processId)
query.respond(QueryResponsePayload.BooleanPayload(hasOpened))
}
is ProcessOutputQuery.SpecifyAdditionalMessageToUser -> {
processMap[query.processId]?.also { internalProcess ->
when (val status = internalProcess.status.value) {
ProcessStatus.Running -> {}
is ProcessStatus.Done -> {
internalProcess.status.emit(
status.copy(
additionalMessageToUser = query.messageToUser,
isCritical = true,
),
)
}
}
}
query.respond(QueryResponsePayload.UnitPayload)
}
}
}
}
}
}
private fun collectSearchStats() {
coroutineScope.launch {
snapshotFlow { processTreeUiState.searchState.text }
.collect {
ProcessOutputUsageCollector.treeSearchEdited()
}
}
}
@OptIn(FlowPreview::class)
private fun collectProcessTree() {
val backgroundErrorProcesses = MutableStateFlow<Set<Int>>(setOf())
val backgroundObservingCoroutines = mutableListOf<Job>()
coroutineScope.launch {
loggedProcesses
.debounce(100.milliseconds)
.collect { list ->
for (coroutine in backgroundObservingCoroutines) {
coroutine.cancelAndJoin()
}
backgroundObservingCoroutines.clear()
backgroundErrorProcesses.value = setOf()
list
.filter {
val kind =
it.data.traceContextUuid
?.let { uuid -> resolveTraceContext(uuid) }
?.kind
when (kind) {
TraceContextKind.NON_INTERACTIVE -> true
TraceContextKind.INTERACTIVE, null -> false
}
}
.forEach { process ->
val exitData = when (val status = process.status.value) {
ProcessStatus.Running -> null
is ProcessStatus.Done -> status
}
if (exitData != null) {
if (exitData.exitCode != 0) {
backgroundErrorProcesses.value += process.data.id
}
return@forEach
}
backgroundObservingCoroutines +=
launch(CoroutineName(CoroutineNames.EXIT_INFO_COLLECTOR)) {
process.status.collect {
when (it) {
is ProcessStatus.Done if it.exitCode != 0 ->
backgroundErrorProcesses.value += process.data.id
else ->
backgroundErrorProcesses.value -= process.data.id
}
}
}
}
}
}
combine(
backgroundErrorProcesses,
loggedProcesses.debounce(100.milliseconds),
snapshotFlow { processTreeUiState.searchState.text },
snapshotFlow { processTreeUiState.filters.active.toSet() },
)
{ backgroundErrorProcesses, processList, search, filters ->
val lowercaseSearch = search.toString().trim().lowercase()
var filteredProcesses =
processList
.reversed()
.filter {
it.data.shortenedCommandString
.lowercase()
.contains(lowercaseSearch)
}
if (!filters.contains(TreeFilter.Item.SHOW_BACKGROUND_PROCESSES)) {
filteredProcesses = filteredProcesses.filter {
val kind = it.data.traceContextUuid
?.let { uuid -> resolveTraceContext(uuid) }
?.kind
kind != TraceContextKind.NON_INTERACTIVE ||
backgroundErrorProcesses.contains(it.data.id)
}
}
data class Node(
val traceContext: TraceContextDto? = null,
val process: LoggedProcess? = null,
val children: MutableList<Node> = mutableListOf(),
)
val root = mutableListOf<Node>()
filteredProcesses.forEach { process ->
val traceContext =
process.data.traceContextUuid
?.let { resolveTraceContext(it) }
when {
traceContext == null || traceContext.kind == TraceContextKind.NON_INTERACTIVE ->
root += Node(process = process)
else -> {
val hierarchy = traceContext.hierarchy()
var currentRoot = root
hierarchy.forEach { currentContext ->
val node =
currentRoot
.firstOrNull { node ->
node.traceContext
?.let { it.uuid == currentContext.uuid } == true
}
?: Node(traceContext = currentContext).also {
currentRoot += it
}
currentRoot = node.children
}
currentRoot += Node(process = process)
}
}
}
fun TreeGeneratorScope<TreeNode>.buildNodeTree(
root: List<Node>,
) {
root.forEach { (traceContext, process, children) ->
if (traceContext != null) {
addNode(
TreeNode.Context(traceContext),
traceContext,
) {
buildNodeTree(children)
}
} else if (process != null) {
addLeaf(
TreeNode.Process(
process,
resolveProcessIcon(process),
),
process,
)
}
}
}
val newTree = buildTree {
buildNodeTree(root)
}
if (newTree.isEmpty()) {
selectProcess(null)
}
processTree.value = newTree
}.launchIn(coroutineScope)
}
private fun ensureProcessTreeScroll() {
coroutineScope.launch(Dispatchers.EDT) {
combine(
snapshotFlow { processTreeUiState.selectableLazyListState.firstVisibleItemIndex },
shouldScrollToTop,
) { firstVisibleIndex, processes -> (firstVisibleIndex > 0) to processes }
.collect { (canScrollBackwards, shouldScrollToTopValue) ->
if (canScrollBackwards && shouldScrollToTopValue) {
shouldScrollToTop.value = false
processTreeUiState.selectableLazyListState.scrollToItem(0)
}
}
}
}
private fun resolveProcessIcon(loggedProcess: LoggedProcess): ProcessIcon? {
iconCache[loggedProcess]?.also {
return it
}
val exe = loggedProcess.data.exe.parts.lastOrNull() ?: return null
val exeWithoutExt = exe.substringBeforeLast('.')
iconMapping[ProcessBinaryFileName(exeWithoutExt)]?.also {
iconCache[loggedProcess] = it
return it
}
for (matcher in iconMatchers) {
if (matcher.matcher(ProcessBinaryFileName(exeWithoutExt))) {
iconCache[loggedProcess] = matcher.icon
return matcher.icon
}
}
return null
}
private fun TraceContextDto.hierarchy(): List<TraceContextDto> {
val hierarchy = mutableListOf<TraceContextDto>()
var currentContext: TraceContextDto? = this
while (currentContext != null) {
hierarchy.add(0, currentContext)
currentContext = currentContext.parentUuid?.let { traceContextCache[it] }
}
return hierarchy
}
}
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")
val maxLength: Int =
Tag::class.java.declaredFields
.filter { it.type == String::class.java }
.fastMaxOfOrDefault(0) { (it.get(null) as String).length }
}
private fun <K, V> boundedLinkedHashMap(maxSize: Int): LinkedHashMap<K, V> =
object : LinkedHashMap<K, V>(maxSize) {
override fun removeEldestEntry(eldest: Map.Entry<K, V>): Boolean =
size > maxSize
}

View File

@@ -1,15 +1,15 @@
package com.intellij.python.processOutput.impl
package com.intellij.python.processOutput.frontend
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.python.processOutput.ProcessBinaryFileName
import com.intellij.python.processOutput.ProcessIcon
import com.intellij.python.processOutput.ProcessMatcher
import com.intellij.python.processOutput.ProcessOutputIconMapping
import com.intellij.python.processOutput.common.ProcessBinaryFileName
import com.intellij.python.processOutput.common.ProcessIcon
import com.intellij.python.processOutput.common.ProcessMatcher
import com.intellij.python.processOutput.common.ProcessOutputIconMapping
internal object ProcessOutputIconMappingData {
private val EP_NAME =
ExtensionPointName<ProcessOutputIconMapping>(
"com.intellij.python.processOutput.processOutputIconMapping",
"com.intellij.python.processOutput.common.processOutputIconMapping",
)
val mapping: Map<ProcessBinaryFileName, ProcessIcon>

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl
package com.intellij.python.processOutput.frontend
import androidx.compose.runtime.remember
import com.intellij.openapi.components.service
@@ -6,8 +6,8 @@ import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.python.processOutput.impl.ProcessOutputBundle.message
import com.intellij.python.processOutput.impl.ui.components.ToolWindow
import com.intellij.python.processOutput.frontend.ProcessOutputBundle.message
import com.intellij.python.processOutput.frontend.ui.components.ToolWindow
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.bridge.addComposeTab
@@ -34,5 +34,3 @@ class ProcessOutputToolWindowFactory : ToolWindowFactory, DumbAware {
}
}
}

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl
package com.intellij.python.processOutput.frontend
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
@@ -7,29 +7,34 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object ProcessOutputUsageCollector : CounterUsagesCollector() {
private val GROUP: EventLogGroup = EventLogGroup("pycharm.processOutputToolWindow", 2)
private val GROUP: EventLogGroup = EventLogGroup("pycharm.processOutputToolWindow", 3)
private val TOGGLED_FIELD = EventFields.Boolean("enabled")
private val TOGGLED_TREE_FILTER =
EventFields.Enum("tree_filter_variant", TreeFilter.Item::class.java)
private val TOGGLED_OUTPUT_FILTER =
EventFields.Enum("output_filter_variant", OutputFilter.Item::class.java)
private val TOGGLED_FILTER_ENABLED = EventFields.Boolean("filter_enabled")
private val TREE_PROCESS_SELECTED = GROUP.registerEvent("tree.processSelected")
private val TREE_SEARCH_EDITED = GROUP.registerEvent("tree.searchEdited")
private val TREE_FILTER_TIME_TOGGLED = GROUP.registerVarargEvent(
"tree.filter.timeToggled",
TOGGLED_FIELD,
)
private val TREE_FILTER_BACKGROUND_PROCESSES_TOGGLED = GROUP.registerVarargEvent(
"tree.filter.backgroundProcessesToggled",
TOGGLED_FIELD,
private val TREE_FILTER_TOGGLED = GROUP.registerVarargEvent(
"tree.filterToggled",
TOGGLED_TREE_FILTER,
TOGGLED_FILTER_ENABLED,
)
private val TREE_EXPAND_ALL_CLICKED = GROUP.registerEvent("tree.expandAllClicked")
private val TREE_COLLAPSE_ALL_CLICKED = GROUP.registerEvent("tree.collapseAllClicked")
private val OUTPUT_FILTER_SHOW_TAGS_TOGGLED = GROUP.registerVarargEvent(
"output.filter.showTagsToggled",
TOGGLED_FIELD,
private val OUTPUT_FILTER_TOGGLED = GROUP.registerVarargEvent(
"output.filterToggled",
TOGGLED_OUTPUT_FILTER,
TOGGLED_FILTER_ENABLED,
)
private val OUTPUT_COPY_CLICKED = GROUP.registerVarargEvent("output.copyClicked")
private val OUTPUT_TAG_SECTION_COPY_CLICKED = GROUP.registerVarargEvent("output.copyTagSectionClicked")
private val OUTPUT_EXIT_INFO_COPY_CLICKED = GROUP.registerVarargEvent("output.copyExitInfoClicked")
private val OUTPUT_TAG_SECTION_COPY_CLICKED =
GROUP.registerVarargEvent("output.copyTagSectionClicked")
private val OUTPUT_EXIT_INFO_COPY_CLICKED =
GROUP.registerVarargEvent("output.copyExitInfoClicked")
private val OUTPUT_PROCESS_INFO_TOGGLED = GROUP.registerVarargEvent(
"output.processInfoToggled",
TOGGLED_FIELD,
@@ -50,15 +55,10 @@ object ProcessOutputUsageCollector : CounterUsagesCollector() {
TREE_SEARCH_EDITED.log()
}
fun treeFilterTimeToggled(enabled: Boolean) {
TREE_FILTER_TIME_TOGGLED.log(
TOGGLED_FIELD.with(enabled),
)
}
fun treeFilterBackgroundProcessesToggled(enabled: Boolean) {
TREE_FILTER_BACKGROUND_PROCESSES_TOGGLED.log(
TOGGLED_FIELD.with(enabled),
fun treeFilterToggled(treeFilterItem: TreeFilter.Item, enabled: Boolean) {
TREE_FILTER_TOGGLED.log(
TOGGLED_TREE_FILTER.with(treeFilterItem),
TOGGLED_FILTER_ENABLED.with(enabled),
)
}
@@ -70,9 +70,10 @@ object ProcessOutputUsageCollector : CounterUsagesCollector() {
TREE_COLLAPSE_ALL_CLICKED.log()
}
fun outputFilterShowTagsToggled(enabled: Boolean) {
OUTPUT_FILTER_SHOW_TAGS_TOGGLED.log(
TOGGLED_FIELD.with(enabled),
fun outputFilterToggled(outputFilterItem: OutputFilter.Item, enabled: Boolean) {
OUTPUT_FILTER_TOGGLED.log(
TOGGLED_OUTPUT_FILTER.with(outputFilterItem),
TOGGLED_FILTER_ENABLED.with(enabled),
)
}

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl
package com.intellij.python.processOutput.frontend
import java.time.ZoneId
import java.time.format.DateTimeFormatter

View File

@@ -1,5 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.processOutput.impl.icons;
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.processOutput.frontend.icons;
import com.intellij.ui.IconManager;
import org.jetbrains.annotations.NotNull;
@@ -10,10 +10,9 @@ import javax.swing.*;
* NOTE THIS FILE IS AUTO-GENERATED
* DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead
*/
@org.jetbrains.annotations.ApiStatus.Internal
public final class PythonProcessOutputImplIcons {
public final class PythonProcessOutputFrontendIcons {
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, PythonProcessOutputImplIcons.class.getClassLoader(), cacheKey, flags);
return IconManager.getInstance().loadRasterizedIcon(path, PythonProcessOutputFrontendIcons.class.getClassLoader(), cacheKey, flags);
}
/** 16x16 */ public static final @NotNull Icon Process = load("icons/process.svg", -2015894419, 2);
/** 16x16 */ public static final @NotNull Icon ProcessHeavy = load("icons/processHeavy.svg", 1632738849, 2);

View File

@@ -0,0 +1,4 @@
@ApiStatus.Internal
package com.intellij.python.processOutput.frontend;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui
package com.intellij.python.processOutput.frontend.ui
import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified

View File

@@ -1,6 +1,6 @@
package com.intellij.python.processOutput.impl.ui
package com.intellij.python.processOutput.frontend.ui
import com.intellij.python.processOutput.impl.icons.PythonProcessOutputImplIcons
import com.intellij.python.processOutput.frontend.icons.PythonProcessOutputFrontendIcons
import javax.swing.Icon
import org.jetbrains.jewel.bridge.icon.fromPlatformIcon
import org.jetbrains.jewel.ui.icon.IntelliJIconKey
@@ -8,10 +8,10 @@ import org.jetbrains.jewel.ui.icons.AllIconsKeys
internal object Icons {
object Keys {
val Process = PythonProcessOutputImplIcons.Process.toJewelKey()
val ResultIncorrect = PythonProcessOutputImplIcons.ResultIncorrect.toJewelKey()
val ProcessHeavy = PythonProcessOutputImplIcons.ProcessHeavy.toJewelKey()
val ProcessMedium = PythonProcessOutputImplIcons.ProcessMedium.toJewelKey()
val Process = PythonProcessOutputFrontendIcons.Process.toJewelKey()
val ResultIncorrect = PythonProcessOutputFrontendIcons.ResultIncorrect.toJewelKey()
val ProcessHeavy = PythonProcessOutputFrontendIcons.ProcessHeavy.toJewelKey()
val ProcessMedium = PythonProcessOutputFrontendIcons.ProcessMedium.toJewelKey()
val Filter = AllIconsKeys.Actions.Show
val Dropdown = AllIconsKeys.General.Dropdown
val ExpandAll = AllIconsKeys.Actions.Expandall
@@ -28,5 +28,5 @@ internal object Icons {
}
private fun Icon.toJewelKey() =
IntelliJIconKey.fromPlatformIcon(this, PythonProcessOutputImplIcons::class.java)
IntelliJIconKey.fromPlatformIcon(this, PythonProcessOutputFrontendIcons::class.java)
}

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Row
@@ -8,8 +8,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.intellij.python.processOutput.impl.ProcessOutputBundle.message
import com.intellij.python.processOutput.impl.ui.Icons
import com.intellij.python.processOutput.frontend.ProcessOutputBundle.message
import com.intellij.python.processOutput.frontend.ui.Icons
import org.jetbrains.jewel.foundation.modifier.thenIf
import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.IconButton

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -16,10 +16,10 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.intellij.python.processOutput.impl.ProcessOutputBundle.message
import com.intellij.python.processOutput.impl.ui.Icons
import com.intellij.python.processOutput.impl.ui.expandable
import com.intellij.python.processOutput.impl.ui.isExpanded
import com.intellij.python.processOutput.frontend.ProcessOutputBundle.message
import com.intellij.python.processOutput.frontend.ui.Icons
import com.intellij.python.processOutput.frontend.ui.expandable
import com.intellij.python.processOutput.frontend.ui.isExpanded
import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.scrollbarContentSafePadding

View File

@@ -1,11 +1,11 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.intellij.python.processOutput.impl.ui.Colors
import com.intellij.python.processOutput.frontend.ui.Colors
import org.jetbrains.jewel.ui.component.Text
@Composable

View File

@@ -1,7 +1,7 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -10,17 +10,19 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateSet
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.intellij.python.processOutput.impl.ProcessOutputBundle.message
import com.intellij.python.processOutput.impl.ui.Icons
import com.intellij.python.processOutput.impl.ui.thenIfNotNull
import kotlinx.collections.immutable.ImmutableList
import com.intellij.python.processOutput.frontend.ProcessOutputBundle.message
import com.intellij.python.processOutput.frontend.ui.Icons
import com.intellij.python.processOutput.frontend.ui.thenIfNotNull
import com.intellij.python.processOutput.frontend.ui.toggle
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.modifier.border
@@ -33,16 +35,42 @@ import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.items
import org.jetbrains.jewel.ui.theme.iconButtonStyle
@ApiStatus.Internal
class FilterActionGroupState<TFilter, TItem>(treeFilter: TFilter)
where TItem : Enum<TItem>,
TItem : FilterItem,
TFilter : Filter<TItem> {
internal val active: SnapshotStateSet<TItem> = mutableStateSetOf()
init {
active.addAll(treeFilter.defaultActive)
}
}
@ApiStatus.Internal
interface Filter<TItem>
where TItem : Enum<TItem>,
TItem : FilterItem {
val defaultActive: Set<TItem>
}
@ApiStatus.Internal
interface FilterItem {
val title: String
val testTag: String
}
@Composable
internal fun <T : FilterItem> FilterActionGroup(
internal inline fun <TFilter, reified TItem> FilterActionGroup(
tooltipText: String,
items: ImmutableList<FilterEntry<T>>,
isSelected: (T) -> Boolean,
onItemClick: (T) -> Unit,
state: FilterActionGroupState<TFilter, TItem>,
crossinline onFilterItemToggled: (filterItem: TItem, enabled: Boolean) -> Unit,
enabled: Boolean = true,
modifier: Modifier = Modifier,
menuModifier: Modifier = Modifier,
) {
) where TItem : Enum<TItem>,
TItem : FilterItem,
TFilter : Filter<TItem> {
var isMenuOpen by remember { mutableStateOf(false) }
Box {
@@ -83,18 +111,21 @@ internal fun <T : FilterItem> FilterActionGroup(
modifier = menuModifier.onHover { isHovered = it },
) {
items(
items = items,
isSelected = { isSelected(it.item) },
onItemClick = { onItemClick(it.item) },
items = enumValues<TItem>().toList(),
isSelected = { state.active.contains(it) },
onItemClick = {
state.active.toggle(it)
onFilterItemToggled(it, state.active.contains(it))
},
) {
Row(
modifier = Modifier.thenIfNotNull(it.testTag) { tag ->
testTag(tag)
},
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
horizontalArrangement = spacedBy(8.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically,
) {
if (isSelected(it.item)) {
if (state.active.contains(it)) {
Icon(
Icons.Keys.Checked,
message("process.output.icon.description.checked"),
@@ -104,7 +135,7 @@ internal fun <T : FilterItem> FilterActionGroup(
Spacer(Modifier.width(16.dp))
}
Text(it.item.title)
Text(it.title)
}
}
}
@@ -112,16 +143,6 @@ internal fun <T : FilterItem> FilterActionGroup(
}
}
@ApiStatus.Internal
interface FilterItem {
val title: String
}
internal data class FilterEntry<T : FilterItem>(
val item: T,
val testTag: String? = null,
)
internal object FilterActionGroupTestTags {
const val CHECKED_ICON = "ProcessOutput.FilterActionGroup.CheckedIcon"
}

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.layout.Arrangement
@@ -30,17 +30,18 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.intellij.python.community.execService.impl.LoggedProcessLine
import com.intellij.python.processOutput.impl.OutputFilter
import com.intellij.python.processOutput.impl.ProcessOutputBundle.message
import com.intellij.python.processOutput.impl.ProcessOutputController
import com.intellij.python.processOutput.impl.Tag
import com.intellij.python.processOutput.impl.formatFull
import com.intellij.python.processOutput.impl.ui.Colors
import com.intellij.python.processOutput.impl.ui.Icons
import com.intellij.python.processOutput.impl.ui.collectReplayAsState
import com.intellij.python.processOutput.impl.ui.thenIfNotNull
import kotlinx.collections.immutable.persistentListOf
import com.intellij.python.processOutput.common.OutputKindDto
import com.intellij.python.processOutput.frontend.OutputFilter
import com.intellij.python.processOutput.frontend.ProcessOutputBundle.message
import com.intellij.python.processOutput.frontend.ProcessOutputController
import com.intellij.python.processOutput.frontend.ProcessStatus
import com.intellij.python.processOutput.frontend.Tag
import com.intellij.python.processOutput.frontend.formatFull
import com.intellij.python.processOutput.frontend.ui.Colors
import com.intellij.python.processOutput.frontend.ui.Icons
import com.intellij.python.processOutput.frontend.ui.commandString
import com.intellij.python.processOutput.frontend.ui.shortenedCommandString
import com.intellij.python.processOutput.frontend.ui.thenIfNotNull
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer
@@ -64,7 +65,7 @@ internal fun OutputSection(controller: ProcessOutputController) {
Box(modifier = Modifier.weight(1f)) {
selectedProcess?.also {
InterText(
text = it.shortenedCommandString,
text = it.data.shortenedCommandString,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.SemiBold,
)
@@ -73,14 +74,10 @@ internal fun OutputSection(controller: ProcessOutputController) {
FilterActionGroup(
tooltipText = message("process.output.viewOptions.tooltip"),
items = persistentListOf(
FilterEntry(
item = OutputFilter.ShowTags,
testTag = OutputSectionTestTags.FILTERS_TAGS,
),
),
isSelected = { controller.processOutputUiState.filters.contains(it) },
onItemClick = { controller.toggleOutputFilter(it) },
state = controller.processOutputUiState.filters,
onFilterItemToggled = { filterItem, enabled ->
controller.onOutputFilterItemToggled(filterItem, enabled)
},
modifier = Modifier.testTag(OutputSectionTestTags.FILTERS_BUTTON),
menuModifier = Modifier.testTag(OutputSectionTestTags.FILTERS_MENU),
)
@@ -103,15 +100,15 @@ internal fun OutputSection(controller: ProcessOutputController) {
modifier = Modifier.fillMaxSize(),
scrollState = listState as ScrollableState,
) {
val lines by loggedProcess.lines.collectReplayAsState()
val exitInfo by loggedProcess.exitInfo.collectAsState()
val lines = remember(loggedProcess) { loggedProcess.lines }
val status by loggedProcess.status.collectAsState()
val isInfoExpandedState =
controller.processOutputUiState.isInfoExpanded.collectAsState()
val isOutputExpandedState =
controller.processOutputUiState.isOutputExpanded.collectAsState()
val isDisplayTags = controller.processOutputUiState.filters.contains(
OutputFilter.ShowTags,
val isDisplayTags = controller.processOutputUiState.filters.active.contains(
OutputFilter.Item.SHOW_TAGS,
)
SelectionContainer {
@@ -128,19 +125,19 @@ internal fun OutputSection(controller: ProcessOutputController) {
infoLineItems(
InfoLine.Single(
message("process.output.output.sections.info.started"),
loggedProcess.startedAt.formatFull(),
loggedProcess.data.startedAt.formatFull(),
),
InfoLine.Single(
message("process.output.output.sections.info.command"),
loggedProcess.commandString,
loggedProcess.data.commandString,
),
loggedProcess.pid?.let { pid ->
loggedProcess.data.pid?.let { pid ->
InfoLine.Single(
message("process.output.output.sections.info.pid"),
pid.toString(),
)
},
loggedProcess.cwd?.let { cwd ->
loggedProcess.data.cwd?.let { cwd ->
InfoLine.Single(
message("process.output.output.sections.info.cwd"),
cwd,
@@ -148,11 +145,11 @@ internal fun OutputSection(controller: ProcessOutputController) {
},
InfoLine.Single(
message("process.output.output.sections.info.target"),
loggedProcess.target,
loggedProcess.data.target,
),
InfoLine.Multi(
message("process.output.output.sections.info.env"),
loggedProcess.env.entries.map { (key, value) ->
loggedProcess.data.env.entries.map { (key, value) ->
"$key=$value"
},
),
@@ -194,36 +191,39 @@ internal fun OutputSection(controller: ProcessOutputController) {
)
}
exitInfo?.also { exitInfo ->
item(key = "exit") {
OutputLine(
displayTags = isDisplayTags,
sectionIndicator =
SectionIndicator(
Tag.EXIT,
OutputSectionTestTags.COPY_OUTPUT_EXIT_INFO_BUTTON,
) {
controller.copyOutputExitInfoToClipboard(
loggedProcess,
)
},
text = buildString {
append(exitInfo.exitValue)
exitInfo.additionalMessageToUser?.also { message ->
append(": ")
append(message)
}
},
textStyle = SpanStyle(
color =
if (exitInfo.exitValue != 0) {
Colors.ErrorText
} else {
Color.Unspecified
when (val status = status) {
ProcessStatus.Running -> {}
is ProcessStatus.Done -> {
item(key = "exit") {
OutputLine(
displayTags = isDisplayTags,
sectionIndicator =
SectionIndicator(
Tag.EXIT,
OutputSectionTestTags.COPY_OUTPUT_EXIT_INFO_BUTTON,
) {
controller.copyOutputExitInfoToClipboard(
loggedProcess,
)
},
),
)
text = buildString {
append(status.exitCode)
status.additionalMessageToUser?.also { message ->
append(": ")
append(message)
}
},
textStyle = SpanStyle(
color =
if (status.exitCode != 0) {
Colors.ErrorText
} else {
Color.Unspecified
},
),
)
}
}
}
}
@@ -427,11 +427,11 @@ private sealed class InfoLine {
data class Multi(override val key: String, val values: List<String?>) : InfoLine()
}
private val LoggedProcessLine.Kind.tag
private val OutputKindDto.tag
get() =
when (this) {
LoggedProcessLine.Kind.ERR -> Tag.ERROR
LoggedProcessLine.Kind.OUT -> Tag.OUTPUT
OutputKindDto.OUT -> Tag.ERROR
OutputKindDto.ERR -> Tag.OUTPUT
}
internal object OutputSectionTestTags {

View File

@@ -1,10 +1,10 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.intellij.python.processOutput.impl.ProcessOutputController
import com.intellij.python.processOutput.frontend.ProcessOutputController
import org.jetbrains.jewel.ui.component.HorizontalSplitLayout
import org.jetbrains.jewel.ui.component.rememberSplitLayoutState

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
@@ -43,19 +43,20 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.intellij.python.community.execService.ConcurrentProcessWeight
import com.intellij.python.processOutput.impl.ProcessOutputBundle.message
import com.intellij.python.processOutput.impl.ProcessOutputController
import com.intellij.python.processOutput.impl.TreeFilter
import com.intellij.python.processOutput.impl.TreeNode
import com.intellij.python.processOutput.impl.formatTime
import com.intellij.python.processOutput.impl.ui.Colors
import com.intellij.python.processOutput.impl.ui.Icons
import com.intellij.python.processOutput.impl.ui.processIsBackground
import com.intellij.python.processOutput.impl.ui.processIsError
import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT
import com.intellij.python.processOutput.common.ProcessWeightDto
import com.intellij.python.processOutput.common.TraceContextKind
import com.intellij.python.processOutput.frontend.ProcessOutputBundle.message
import com.intellij.python.processOutput.frontend.ProcessOutputController
import com.intellij.python.processOutput.frontend.ProcessStatus
import com.intellij.python.processOutput.frontend.TreeFilter
import com.intellij.python.processOutput.frontend.TreeNode
import com.intellij.python.processOutput.frontend.formatTime
import com.intellij.python.processOutput.frontend.ui.Colors
import com.intellij.python.processOutput.frontend.ui.Icons
import com.intellij.python.processOutput.frontend.ui.processIsBackground
import com.intellij.python.processOutput.frontend.ui.processIsError
import com.intellij.python.processOutput.frontend.ui.shortenedCommandString
import kotlin.time.Instant
import kotlinx.collections.immutable.persistentListOf
import org.jetbrains.jewel.bridge.icon.fromPlatformIcon
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.lazy.tree.Tree
@@ -101,6 +102,13 @@ private enum class ErrorKind {
CRITICAL,
}
private data class ProcessWeightViewModel(
val iconKey: IconKey,
val contentDesc: String,
val tooltipMessage: String,
val testId: String,
)
@Composable
internal fun TreeSection(controller: ProcessOutputController) {
Column(modifier = Modifier.fillMaxSize()) {
@@ -178,18 +186,10 @@ private fun TreeToolbar(
FilterActionGroup(
tooltipText = message("process.output.viewOptions.tooltip"),
items = persistentListOf(
FilterEntry(
item = TreeFilter.ShowTime,
testTag = TreeSectionTestTags.FILTERS_TIME,
),
FilterEntry(
item = TreeFilter.ShowBackgroundProcesses,
testTag = TreeSectionTestTags.FILTERS_BACKGROUND,
),
),
isSelected = { controller.processTreeUiState.filters.contains(it) },
onItemClick = { controller.toggleTreeFilter(it) },
state = controller.processTreeUiState.filters,
onFilterItemToggled = { filterItem, enabled ->
controller.onTreeFilterItemToggled(filterItem, enabled)
},
modifier = Modifier.testTag(TreeSectionTestTags.FILTERS_BUTTON),
menuModifier = Modifier.testTag(TreeSectionTestTags.FILTERS_MENU),
)
@@ -268,7 +268,12 @@ private fun TreeContent(controller: ProcessOutputController, tree: Tree<TreeNode
),
interactionSource = remember { MutableInteractionSource() },
) {
TreeRow(it.data, filters.contains(TreeFilter.ShowTime))
TreeRow(
controller,
it.data,
filters.active.contains(TreeFilter.Item.SHOW_TIME),
filters.active.contains(TreeFilter.Item.SHOW_PROCESS_WEIGHT),
)
}
}
}
@@ -284,8 +289,10 @@ private fun TreeContent(controller: ProcessOutputController, tree: Tree<TreeNode
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun TreeRow(
controller: ProcessOutputController,
node: TreeNode,
isTimeDisplayed: Boolean,
isProcessWeightDisplayed: Boolean,
) {
Row(
modifier = Modifier.fillMaxWidth().height(TreeSectionStyling.TREE_ROW_HEIGHT),
@@ -329,23 +336,29 @@ private fun TreeRow(
}
is TreeNode.Process -> {
val icon = node.icon
val exitInfo by node.process.exitInfo.collectAsState()
val errorKind = remember(exitInfo) {
val isError = exitInfo?.takeIf { it.exitValue != 0 } != null
when {
!isError -> ErrorKind.NONE
isError && exitInfo?.isCritical == false -> ErrorKind.NORMAL
val status by node.process.status.collectAsState()
val errorKind = remember(status) {
when (val status = status) {
ProcessStatus.Running -> ErrorKind.NONE
is ProcessStatus.Done if status.exitCode == 0 -> ErrorKind.NONE
is ProcessStatus.Done if !status.isCritical -> ErrorKind.NORMAL
else -> ErrorKind.CRITICAL
}
}
val isBackground = node.process.traceContext == NON_INTERACTIVE_ROOT_TRACE_CONTEXT
val isRunning = remember(exitInfo) { exitInfo == null }
val kind =
node.process.data.traceContextUuid
?.let { controller.resolveTraceContext(it) }
?.kind
val isBackground = when (kind) {
TraceContextKind.NON_INTERACTIVE -> true
TraceContextKind.INTERACTIVE, null -> false
}
val isRunning = remember(status) { status == ProcessStatus.Running }
Tooltip(
tooltip = {
Row {
Text(node.process.shortenedCommandString)
Text(node.process.data.shortenedCommandString)
}
},
modifier = Modifier.weight(1f),
@@ -377,7 +390,7 @@ private fun TreeRow(
)
InterText(
text = node.process.shortenedCommandString,
text = node.process.data.shortenedCommandString,
overflow = TextOverflow.Ellipsis,
color =
when (errorKind) {
@@ -390,17 +403,18 @@ private fun TreeRow(
}
}
when (node) {
is TreeNode.Process ->
ProcessWeightIcon(node.process.weight)
is TreeNode.Context ->
{}
if (isProcessWeightDisplayed) {
when (node) {
is TreeNode.Process ->
ProcessWeightIcon(node.process.data.weight)
is TreeNode.Context -> {}
}
}
if (isTimeDisplayed) {
val instant = when (node) {
is TreeNode.Context -> Instant.fromEpochMilliseconds(node.traceContext.timestamp)
is TreeNode.Process -> node.process.startedAt
is TreeNode.Process -> node.process.data.startedAt
}
InterText(
@@ -502,43 +516,41 @@ private fun ProcessIcon(
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProcessWeightIcon(weight: ConcurrentProcessWeight?) {
fun ProcessWeightIcon(weight: ProcessWeightDto?) {
when (weight) {
ConcurrentProcessWeight.MEDIUM, ConcurrentProcessWeight.HEAVY -> {
val iconKey: IconKey
val contentDesc: String
val tooltipMessage: String
when (weight) {
ConcurrentProcessWeight.MEDIUM -> {
iconKey = Icons.Keys.ProcessMedium
contentDesc = message("process.output.icon.description.mediumProcess")
tooltipMessage = message("process.output.tree.weight.medium.tooltip")
}
ConcurrentProcessWeight.HEAVY -> {
iconKey = Icons.Keys.ProcessHeavy
contentDesc = message("process.output.icon.description.heavyProcess")
tooltipMessage = message("process.output.tree.weight.heavy.tooltip")
}
ProcessWeightDto.MEDIUM, ProcessWeightDto.HEAVY -> {
val viewModel = when (weight) {
ProcessWeightDto.MEDIUM -> ProcessWeightViewModel(
iconKey = Icons.Keys.ProcessMedium,
contentDesc = message("process.output.icon.description.mediumProcess"),
tooltipMessage = message("process.output.tree.weight.medium.tooltip"),
testId = TreeSectionTestTags.PROCESS_ITEM_WEIGHT_MEDIUM_ICON,
)
ProcessWeightDto.HEAVY -> ProcessWeightViewModel(
iconKey = Icons.Keys.ProcessHeavy,
contentDesc = message("process.output.icon.description.heavyProcess"),
tooltipMessage = message("process.output.tree.weight.heavy.tooltip"),
testId = TreeSectionTestTags.PROCESS_ITEM_WEIGHT_HEAVY_ICON,
)
}
Tooltip(
tooltip = {
Row {
Text(tooltipMessage)
Text(viewModel.tooltipMessage)
}
},
) {
Icon(
key = iconKey,
contentDescription = contentDesc,
modifier = Modifier.padding(
TreeSectionStyling.TREE_ITEM_PROCESS_WEIGHT_PADDING
)
key = viewModel.iconKey,
contentDescription = viewModel.contentDesc,
modifier = Modifier
.padding(TreeSectionStyling.TREE_ITEM_PROCESS_WEIGHT_PADDING)
.testTag(viewModel.testId),
)
}
}
null, ConcurrentProcessWeight.LIGHT -> {}
null, ProcessWeightDto.LIGHT -> {}
}
}
@@ -548,6 +560,8 @@ internal object TreeSectionTestTags {
const val PROCESS_ITEM_BACK_ICON = "ProcessOutput.Tree.ProcessItemBackIcon"
const val PROCESS_ITEM_ERROR_ICON = "ProcessOutput.Tree.ProcessItemErrorIcon"
const val PROCESS_ITEM_ICON = "ProcessOutput.Tree.ProcessItemIcon"
const val PROCESS_ITEM_WEIGHT_MEDIUM_ICON = "ProcessOutput.Tree.Weight.MediumIcon"
const val PROCESS_ITEM_WEIGHT_HEAVY_ICON = "ProcessOutput.Tree.Weight.HeavyIcon"
const val FOLDER_ITEM_ICON = "ProcessOutput.Tree.FolderItemIcon"
const val EXPAND_ALL_BUTTON = "ProcessOutput.Tree.ExpandAllButton"
const val COLLAPSE_ALL_BUTTON = "ProcessOutput.Tree.CollapseAllButton"
@@ -555,4 +569,5 @@ internal object TreeSectionTestTags {
const val FILTERS_MENU = "ProcessOutput.Tree.FiltersMenu"
const val FILTERS_BACKGROUND = "ProcessOutput.Tree.FiltersBackground"
const val FILTERS_TIME = "ProcessOutput.Tree.FiltersTime"
const val FILTERS_PROCESS_WEIGHTS = "ProcessOutput.Tree.FiltersProcessWeights"
}

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui
package com.intellij.python.processOutput.frontend.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.hoverable
@@ -11,6 +11,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import com.intellij.python.processOutput.common.LoggedProcessDto
import java.awt.Cursor
import kotlinx.coroutines.flow.SharedFlow
@@ -56,3 +57,16 @@ internal fun <T> SharedFlow<T>.collectReplayAsState(): State<List<T>> {
return data
}
internal val LoggedProcessDto.commandString: String
get() = commandFromSegments(listOf(exe.path) + args)
/**
* Command string with the full path of the exe trimmed only to the latest segments.
* E.g., `/usr/bin/uv` -> `uv`.
*/
internal val LoggedProcessDto.shortenedCommandString: String
get() = commandFromSegments(listOf(exe.parts.last()) + args)
private fun commandFromSegments(segments: List<String>) =
segments.joinToString(" ")

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui
package com.intellij.python.processOutput.frontend.ui
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver

View File

@@ -0,0 +1,178 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.junit5Tests.env
import androidx.compose.runtime.snapshots.SnapshotStateList
import com.intellij.python.community.execService.impl.LoggingProcess
import com.intellij.python.processOutput.common.ExecutableDto
import com.intellij.python.processOutput.common.LoggedProcessDto
import com.intellij.python.processOutput.frontend.LoggedProcess
import com.intellij.python.processOutput.frontend.ProcessStatus
import com.intellij.python.processOutput.frontend.ui.commandString
import com.intellij.python.processOutput.frontend.ui.shortenedCommandString
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.TestApplication
import com.jetbrains.python.TraceContext
import com.jetbrains.python.errorProcessing.Exe
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
import kotlin.time.Clock
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Instant
private class LoggingTest {
@Nested
inner class LoggedProcessTest {
@Test
fun `commandString is constructed as expected`() {
val process1 = process("/usr/bin/uv", "install", "requests")
assertEquals("/usr/bin/uv install requests", process1.data.commandString)
}
@Test
fun `shortenedCommandString is constructed as expected from multiple segments`() {
val process1 = process("/usr/bin/uv", "install", "requests")
assertEquals("uv install requests", process1.data.shortenedCommandString)
}
@Test
fun `shortenedCommandString is constructed as expected from single segment`() {
val process1 = process("uv", "install", "requests")
assertEquals("uv install requests", process1.data.shortenedCommandString)
}
}
@TestApplication
@Nested
inner class LoggingProcessTest {
@Test
fun `logged process gets created correctly`() = timeoutRunBlocking(timeout = 1.minutes) {
val traceContext = TraceContext("some trace")
val loggingProcess = fakeLoggingProcess(
stdout = "stdout text",
stderr = "stderr text",
exitValue = 10,
pid = 100,
traceContext = traceContext,
startedAt = Instant.fromEpochSeconds(100),
cwd = "/some/cwd",
pathToExe = "/usr/bin/exe",
args = listOf("foo", "bar"),
env = mapOf("foo" to "bar"),
)
val loggedProcess = loggingProcess.loggedProcess
val stdout =
loggingProcess.inputStream.readAllBytes().toString(charset = Charsets.UTF_8)
val stderr =
loggingProcess.errorStream.readAllBytes().toString(charset = Charsets.UTF_8)
assert(traceContext.uuid.toString() == loggedProcess.traceContextUuid?.uuid)
assert(100L == loggedProcess.pid)
assert(Instant.fromEpochSeconds(100) == loggedProcess.startedAt)
assert("/some/cwd" == loggedProcess.cwd)
assert(
ExecutableDto(
path = "/usr/bin/exe",
listOf("usr", "bin", "exe"),
) == loggedProcess.exe,
)
assert(listOf("foo", "bar") == loggedProcess.args)
assert(mapOf("foo" to "bar") == loggedProcess.env)
assert(stdout == "stdout text")
assert(stderr == "stderr text")
loggingProcess.destroy()
}
}
companion object {
val nextId = AtomicInteger()
fun process(vararg command: String) =
LoggedProcess(
data = LoggedProcessDto(
weight = null,
traceContextUuid = null,
pid = 123,
startedAt = Clock.System.now(),
cwd = null,
exe = ExecutableDto(
path = command.first(),
parts = command.first().split(Regex("[/\\\\]+")),
),
args = command.drop(1),
env = mapOf(),
target = "Local",
id = nextId.getAndAdd(1),
),
lines = SnapshotStateList(),
status = MutableStateFlow(ProcessStatus.Running),
)
fun fakeLoggingProcess(
stdout: String = "stdout",
stderr: String = "stderr",
exitValue: Int = 0,
pid: Long = 0,
traceContext: TraceContext? = null,
startedAt: Instant = Instant.fromEpochSeconds(0),
cwd: String? = "/some/cwd",
pathToExe: String = "/usr/bin/exe",
args: List<String> = listOf("foo", "bar"),
env: Map<String, String> = mapOf("foo" to "bar"),
) =
LoggingProcess(
object : Process() {
val stdoutStream = ByteArrayInputStream(stdout.toByteArray())
val stderrStream = ByteArrayInputStream(stderr.toByteArray())
val stdinStream = ByteArrayOutputStream()
val destroyFuture = CompletableFuture<Int>()
override fun getOutputStream(): OutputStream =
stdinStream
override fun getInputStream(): InputStream =
stdoutStream
override fun getErrorStream(): InputStream =
stderrStream
override fun waitFor(): Int {
destroyFuture.get()
return exitValue
}
override fun exitValue(): Int {
return exitValue
}
override fun destroy() {
destroyFuture.complete(10)
}
override fun pid(): Long =
pid
},
null,
traceContext,
startedAt,
cwd,
Exe.fromString(pathToExe),
args,
env,
"Local",
)
}
}

View File

@@ -3,23 +3,24 @@ package com.intellij.python.junit5Tests.env
import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.components.service
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.util.SystemInfoRt
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.LoggedProcess
import com.intellij.python.community.execService.impl.LoggedProcessLine
import com.intellij.python.community.execService.impl.LoggedProcessLine.Kind.OUT
import com.intellij.python.community.execService.impl.LoggingLimits
import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
import com.intellij.python.junit5Tests.framework.env.PythonBinaryPath
import com.intellij.python.processOutput.impl.CoroutineNames
import com.intellij.python.processOutput.impl.ProcessOutputControllerService
import com.intellij.python.processOutput.impl.ProcessOutputControllerServiceLimits
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.ProcessOutputControllerService
import com.intellij.python.processOutput.frontend.ProcessOutputControllerServiceLimits
import com.intellij.python.processOutput.frontend.ProcessStatus
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.common.waitUntil
import com.intellij.testFramework.junit5.fixture.projectFixture
import com.intellij.util.io.awaitExit
import com.intellij.util.system.OS
import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.getOrThrow
@@ -38,6 +39,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.debug.DebugProbes
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.junit.jupiter.api.Assertions.assertEquals
@@ -62,14 +64,14 @@ class ProcessOutputControllerServiceTest {
service.loggedProcesses.collect {
historyUpdates += 1
for (process in it) {
if (!history.contains(process.id)) {
history[process.id] = process
if (!history.contains(process.data.id)) {
history[process.data.id] = process
}
}
}
}
val newLineLen = if (SystemInfoRt.isWindows) 2 else 1
val newLineLen = if (OS.CURRENT == OS.Windows) 2 else 1
val binOnEel = BinOnEel(python, cwd)
val mainPy = Files.createFile(cwd.resolve(MAIN_PY))
@@ -93,10 +95,10 @@ class ProcessOutputControllerServiceTest {
waitUntil {
val index = ProcessOutputControllerServiceLimits.MAX_PROCESSES - 1
val process = history.toList().find { (_, it) ->
it.lines.replayCache.find { it.text == "test $index" } != null
it.lines.find { it.text == "test $index" } != null
}
process != null && process.second.lines.replayCache.size == 3
process != null && process.second.lines.size == 3
}
// the amount of processes logged should exactly equal to MAX_PROCESSES
@@ -114,7 +116,7 @@ class ProcessOutputControllerServiceTest {
assertNotNull(process)
with(process.lines.replayCache) {
with(process.lines) {
// (stdout): test $it
// (stdout): x repeated MAX_OUTPUT_SIZE times minus the length of "test $it" + 1
// (stderr): y repeated MAX_OUTPUT_SIZE times
@@ -123,7 +125,7 @@ class ProcessOutputControllerServiceTest {
val xLen = LoggingLimits.MAX_OUTPUT_SIZE - ("test $it".length + newLineLen)
val yLen = LoggingLimits.MAX_OUTPUT_SIZE
assertTrue(contains(LoggedProcessLine("test $it", OUT)))
assertTrue(contains(OutputLineDto(OutputKindDto.OUT, "test $it", 0)))
assertNotNull(
find { elem ->
elem.text.startsWith("xxx") && elem.text.length == xLen
@@ -148,10 +150,10 @@ class ProcessOutputControllerServiceTest {
waitUntil {
val index = (ProcessOutputControllerServiceLimits.MAX_PROCESSES * 3) - 1
val process = history.toList().find { (_, it) ->
it.lines.replayCache.getOrNull(0)?.text == "test $index"
it.lines.getOrNull(0)?.text == "test $index"
}
process != null && process.second.lines.replayCache.size == 3
process != null && process.second.lines.size == 3
}
// older processes beyond MAX_PROCESSES should be truncated
@@ -170,14 +172,14 @@ class ProcessOutputControllerServiceTest {
assertNotNull(process)
with(process.lines.replayCache) {
with(process.lines) {
// (stdout): test $newIt
// (stdout): x repeated MAX_OUTPUT_SIZE times minus the length of "test $newIt" + 1
// (stderr): y repeated MAX_OUTPUT_SIZE times
val xLen = LoggingLimits.MAX_OUTPUT_SIZE - ("test $newIt".length + newLineLen)
val yLen = LoggingLimits.MAX_OUTPUT_SIZE
assertTrue(contains(LoggedProcessLine("test $newIt", OUT)))
assertTrue(contains(OutputLineDto(OutputKindDto.OUT, "test $newIt", 0)))
assertNotNull(
find { elem ->
elem.text.startsWith("xxx") && elem.text.length == xLen
@@ -216,39 +218,46 @@ class ProcessOutputControllerServiceTest {
)
}
// no exit info collector coroutines should exist
assertEquals(0, exitInfoCollectorCoroutinesCount())
// spawn 10 processes
val processes = mutableListOf<Process>()
repeat(10) {
processes += runBinWithInput(binOnEel, Args(MAIN_PY, it.toString()))
}
// 10 collector coroutines should be active
waitUntil {
exitInfoCollectorCoroutinesCount() == 10
}
// no coroutines should be active (5 for margin of error)
assert(exitInfoCollectorCoroutinesCount() < 5)
// spawn 1024 processes, instantly terminate them
repeat(1024) {
val process = runBinWithInput(binOnEel, Args(MAIN_PY, (it + 10).toString()))
val process = runBinWithInput(binOnEel, Args(MAIN_PY, it.toString()))
inputAndAwaitExit(process)
}
// 10 collection coroutines should be active
// no coroutines should be active (5 for margin of error)
waitUntil {
exitInfoCollectorCoroutinesCount() == 10
exitInfoCollectorCoroutinesCount() < 5
}
// terminating all the processes
// spawn 100 processes
val processes = mutableListOf<Process>()
repeat(100) {
processes += runBinWithInput(binOnEel, Args(MAIN_PY, it.toString()))
}
// at least 100 coroutines should be active
waitUntil {
exitInfoCollectorCoroutinesCount() >= 100
}
// but not more than 105
assert(exitInfoCollectorCoroutinesCount() <= 105)
// terminating all processes
for (process in processes) {
inputAndAwaitExit(process)
}
// no collection coroutines should be active
// updating the flow by adding and terminating one process
val process = runBinWithInput(binOnEel, Args(MAIN_PY, "500"))
inputAndAwaitExit(process)
// no coroutines should be active (5 for margin of error)
waitUntil {
exitInfoCollectorCoroutinesCount() == 0
exitInfoCollectorCoroutinesCount() < 5
}
}
@@ -294,7 +303,7 @@ class ProcessOutputControllerServiceTest {
loggingProcess.inputStream.readAllBytes()
waitUntil {
service.loggedProcesses.value.lastOrNull()?.lines?.replayCache?.size == 6
service.loggedProcesses.value.lastOrNull()?.lines?.size == 6
}
val process = service.loggedProcesses.value.last()
@@ -318,7 +327,7 @@ class ProcessOutputControllerServiceTest {
// reading all stderr
loggingProcess.errorStream.readAllBytes()
waitUntil { process.lines.replayCache.size == 10 }
waitUntil { process.lines.size == 10 }
// stderr section (6..9)
service.copyOutputTagAtIndexToClipboard(process, 6)
@@ -346,8 +355,12 @@ class ProcessOutputControllerServiceTest {
)
// exit info with additional message
process.exitInfo.emit(
process.exitInfo.value?.copy(additionalMessageToUser = "some test message"),
val status = process.status.value as ProcessStatus.Done
(process.status as MutableStateFlow<ProcessStatus>).emit(
status.copy(
additionalMessageToUser = "some test message",
),
)
service.copyOutputExitInfoToClipboard(process)
@@ -385,13 +398,12 @@ class ProcessOutputControllerServiceTest {
}
runBin(binOnEel, Args(MAIN_PY))
var lines: List<LoggedProcessLine>? = null
var lines: List<OutputLineDto>? = null
waitUntil {
service.loggedProcesses.value
.lastOrNull()
?.lines
?.replayCache
?.also {
lines = it
}
@@ -406,7 +418,7 @@ class ProcessOutputControllerServiceTest {
mapOf(
*toList()
.map { (_, process) ->
process.lines.replayCache.firstOrNull()?.text to process
process.lines.firstOrNull()?.text to process
}
.toTypedArray(),
)

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -12,7 +12,7 @@ import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performMouseInput
import com.intellij.python.processOutput.impl.ProcessOutputTest
import com.intellij.python.processOutput.frontend.ProcessOutputTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
@@ -12,8 +12,8 @@ import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import com.intellij.python.processOutput.impl.ProcessOutputTest
import com.intellij.python.processOutput.impl.ui.IsExpanded
import com.intellij.python.processOutput.frontend.ProcessOutputTest
import com.intellij.python.processOutput.frontend.ui.IsExpanded
import kotlin.test.Test
import kotlin.test.assertEquals

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
@@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.unit.dp
import com.intellij.python.processOutput.impl.ProcessOutputTest
import com.intellij.python.processOutput.frontend.ProcessOutputTest
import kotlin.test.Test
internal class EmptyContainerNoticeTest : ProcessOutputTest() {

View File

@@ -1,11 +1,8 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateSet
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.testTag
@@ -20,10 +17,10 @@ import androidx.compose.ui.test.onSiblings
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performMouseInput
import androidx.compose.ui.unit.dp
import com.intellij.python.processOutput.impl.ProcessOutputTest
import com.intellij.python.processOutput.frontend.ProcessOutputTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.collections.immutable.persistentListOf
import org.jetbrains.annotations.ApiStatus
internal class FilterActionGroupTest : ProcessOutputTest() {
@Test
@@ -81,116 +78,109 @@ internal class FilterActionGroupTest : ProcessOutputTest() {
@Test
fun `view filters buttons call their respective functions`() = processOutputTest {
val clicks = mutableMapOf(
TestFilter.Option1 to 0,
TestFilter.Option2 to 0,
TestFilter.Item.OPTION_1 to 0,
TestFilter.Item.OPTION_2 to 0,
)
scaffold(
onItemClick = {
clicks[it] = clicks[it]!!.plus(1)
val state = scaffold(
onFilterItemToggled = { filterItem, _ ->
clicks[filterItem] = clicks[filterItem]!!.plus(1)
},
)
// clicking the view filters button
onNodeWithTag(TestTags.BUTTON).performClick()
// no menu buttons were clicked, no functions were called
assertEquals(0, clicks[TestFilter.Option1])
assertEquals(0, clicks[TestFilter.Option2])
// no menu buttons were clicked, no active filters
assertEquals(state.active.size, 0)
// clicking on option1
onNodeWithText(
TestFilter.Option1.title,
TestFilter.Item.OPTION_1.title,
useUnmergedTree = true,
).performClick()
// option1 should have been called, but not option2
assertEquals(1, clicks[TestFilter.Option1])
assertEquals(0, clicks[TestFilter.Option2])
// option1 should be active
assertEquals(setOf(TestFilter.Item.OPTION_1), state.active)
assertEquals(clicks[TestFilter.Item.OPTION_1], 1)
// clicking on option2
onNodeWithText(
TestFilter.Option2.title,
TestFilter.Item.OPTION_2.title,
useUnmergedTree = true,
).performClick()
// both filters should have been called
assertEquals(1, clicks[TestFilter.Option1])
assertEquals(1, clicks[TestFilter.Option2])
// both filters should be active
assertEquals(
setOf(TestFilter.Item.OPTION_1, TestFilter.Item.OPTION_2),
state.active,
)
assertEquals(clicks[TestFilter.Item.OPTION_1], 1)
assertEquals(clicks[TestFilter.Item.OPTION_2], 1)
}
@Test
fun `view filters buttons include checked icon when selected`() = processOutputTest {
val selected = mutableStateSetOf<TestFilter>(TestFilter.Option2)
scaffold(selectedItems = selected)
val state = scaffold(selectedItems = setOf(TestFilter.Item.OPTION_2))
// clicking the view filters button
onNodeWithTag(TestTags.BUTTON).performClick()
// option1 disabled, option2 enabled
onNodeWithText(TestFilter.Option1.title, useUnmergedTree = true)
onNodeWithText(TestFilter.Item.OPTION_1.title, useUnmergedTree = true)
.onSiblings()
.filter(hasTestTag(FilterActionGroupTestTags.CHECKED_ICON))
.assertCountEquals(0)
onNodeWithText(TestFilter.Option2.title, useUnmergedTree = true)
onNodeWithText(TestFilter.Item.OPTION_2.title, useUnmergedTree = true)
.onSiblings()
.filter(hasTestTag(FilterActionGroupTestTags.CHECKED_ICON))
.assertCountEquals(1)
// enabling option1, disabling option2
selected.remove(TestFilter.Option2)
selected.add(TestFilter.Option1)
state.active.remove(TestFilter.Item.OPTION_2)
state.active.add(TestFilter.Item.OPTION_1)
// option1 enabled, option2 disabled
onNodeWithText(TestFilter.Option1.title, useUnmergedTree = true)
onNodeWithText(TestFilter.Item.OPTION_1.title, useUnmergedTree = true)
.onSiblings()
.filter(hasTestTag(FilterActionGroupTestTags.CHECKED_ICON))
.assertCountEquals(1)
onNodeWithText(TestFilter.Option2.title, useUnmergedTree = true)
onNodeWithText(TestFilter.Item.OPTION_2.title, useUnmergedTree = true)
.onSiblings()
.filter(hasTestTag(FilterActionGroupTestTags.CHECKED_ICON))
.assertCountEquals(0)
}
private fun scaffold(
selectedItems: SnapshotStateSet<TestFilter> = mutableStateSetOf(),
onItemClick: (TestFilter) -> Unit = {},
) {
selectedItems: Set<TestFilter.Item> = setOf(),
onFilterItemToggled: (TestFilter.Item, Boolean) -> Unit = { _, _ -> },
): FilterActionGroupState<TestFilter, TestFilter.Item> {
val state = FilterActionGroupState(TestFilter)
state.active.addAll(selectedItems)
scaffoldTestContent {
val selected = remember { selectedItems }
Box(modifier = Modifier.size(256.dp).padding(16.dp)) {
FilterActionGroup(
tooltipText = DEFAULT_TOOLTIP_TEXT,
items = persistentListOf(
FilterEntry(
item = TestFilter.Option1,
testTag = TestTags.OPTION_1,
),
FilterEntry(
item = TestFilter.Option2,
testTag = TestTags.OPTION_2,
),
),
isSelected = { selected.contains(it) },
onItemClick = onItemClick,
state = state,
onFilterItemToggled = onFilterItemToggled,
modifier = Modifier.testTag(TestTags.BUTTON),
menuModifier = Modifier.testTag(TestTags.MENU),
)
}
}
return state;
}
}
private abstract class TestFilter : FilterItem {
object Option1 : TestFilter() {
override val title = "option1"
@ApiStatus.Internal
private object TestFilter : Filter<TestFilter.Item> {
enum class Item(override val title: String, override val testTag: String) : FilterItem {
OPTION_1("option1", TestTags.OPTION_1),
OPTION_2("option2", TestTags.OPTION_2),
}
object Option2 : TestFilter() {
override val title = "option2"
}
override val defaultActive: Set<Item> = setOf()
}
private object TestTags {

View File

@@ -1,9 +1,9 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.text.AnnotatedString
import com.intellij.python.processOutput.impl.ProcessOutputTest
import com.intellij.python.processOutput.frontend.ProcessOutputTest
import org.junit.Test
internal class InterTextTest : ProcessOutputTest() {

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsEnabled
@@ -6,11 +6,11 @@ import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import com.intellij.python.community.execService.impl.LoggedProcess
import com.intellij.python.community.execService.impl.LoggedProcessExitInfo
import com.intellij.python.community.execService.impl.LoggedProcessLine
import com.intellij.python.processOutput.impl.OutputFilter
import com.intellij.python.processOutput.impl.ProcessOutputTest
import com.intellij.python.processOutput.common.OutputLineDto
import com.intellij.python.processOutput.frontend.LoggedProcess
import com.intellij.python.processOutput.frontend.OutputFilter
import com.intellij.python.processOutput.frontend.ProcessOutputTest
import com.intellij.python.processOutput.frontend.ProcessStatus
import io.mockk.called
import io.mockk.verify
import kotlin.test.BeforeTest
@@ -75,8 +75,13 @@ internal class OutputSectionTest : ProcessOutputTest() {
useUnmergedTree = true,
).performClick()
// tags should have been called
verify(exactly = 1) { controllerSpy.toggleOutputFilter(OutputFilter.ShowTags) }
// tags should have been called with enabled false
verify(exactly = 1) {
controllerSpy.onOutputFilterItemToggled(
OutputFilter.Item.SHOW_TAGS,
false,
)
}
}
@Test
@@ -118,18 +123,18 @@ internal class OutputSectionTest : ProcessOutputTest() {
// 7..9 - stdout
val process = selectTestProcess(
lines = listOf(
outLine("out1"),
outLine("out2"),
outLine("out3"),
outLine("out1", 1),
outLine("out2", 2),
outLine("out3", 3),
errLine("err4"),
errLine("err5"),
errLine("err6"),
errLine("err7"),
errLine("err4", 4),
errLine("err5", 5),
errLine("err6", 6),
errLine("err7", 7),
outLine("out8"),
outLine("out9"),
outLine("out10"),
outLine("out8", 8),
outLine("out9", 9),
outLine("out10", 10),
),
)
@@ -181,10 +186,10 @@ internal class OutputSectionTest : ProcessOutputTest() {
processOutputTest {
// selecting a process with defined exit info
val process = selectTestProcess(
exitInfo =
LoggedProcessExitInfo(
status =
ProcessStatus.Done(
exitedAt = Clock.System.now(),
exitValue = 0,
exitCode = 0,
),
)
@@ -215,18 +220,18 @@ internal class OutputSectionTest : ProcessOutputTest() {
// 6 - exit
selectTestProcess(
lines = listOf(
outLine("out1"),
outLine("out2"),
outLine("out3"),
outLine("out1", 1),
outLine("out2", 2),
outLine("out3", 3),
errLine("err4"),
errLine("err5"),
errLine("err6"),
errLine("err4", 4),
errLine("err5", 5),
errLine("err6", 6),
),
exitInfo =
LoggedProcessExitInfo(
status =
ProcessStatus.Done(
exitedAt = Clock.System.now(),
exitValue = 0,
exitCode = 0,
),
)
@@ -237,7 +242,7 @@ internal class OutputSectionTest : ProcessOutputTest() {
).assertCountEquals(3)
// remove show tags filter
processOutputFilters.remove(OutputFilter.ShowTags)
testProcessOutputUiState.filters.active.remove(OutputFilter.Item.SHOW_TAGS)
// total displayed tags should be 0
onAllNodesWithTag(
@@ -247,8 +252,8 @@ internal class OutputSectionTest : ProcessOutputTest() {
}
private suspend fun selectTestProcess(
lines: List<LoggedProcessLine> = listOf(),
exitInfo: LoggedProcessExitInfo? = null,
lines: List<OutputLineDto> = listOf(),
status: ProcessStatus = ProcessStatus.Running,
): LoggedProcess {
val newProcess =
process(
@@ -258,7 +263,7 @@ internal class OutputSectionTest : ProcessOutputTest() {
"-f",
cwd = "some/random/path",
lines = lines,
exitInfo = exitInfo,
status = status,
)
setSelectedProcess(newProcess)

View File

@@ -1,8 +1,8 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.onAllNodesWithText
import com.intellij.python.processOutput.impl.ProcessOutputTest
import com.intellij.python.processOutput.frontend.ProcessOutputTest
import kotlin.test.Test
import org.jetbrains.jewel.ui.component.Text

View File

@@ -1,4 +1,4 @@
package com.intellij.python.processOutput.impl.ui.components
package com.intellij.python.processOutput.frontend.ui.components
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
@@ -6,20 +6,21 @@ import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.filter
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.isNotEnabled
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onParent
import androidx.compose.ui.test.performClick
import com.intellij.python.processOutput.impl.ProcessOutputTest
import com.intellij.python.processOutput.impl.TreeFilter
import com.intellij.python.processOutput.impl.finish
import com.intellij.python.processOutput.impl.formatTime
import com.intellij.python.processOutput.impl.ui.ProcessIsBackgroundKey
import com.intellij.python.processOutput.impl.ui.ProcessIsErrorKey
import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT
import com.jetbrains.python.TraceContext
import com.intellij.python.processOutput.common.ProcessWeightDto
import com.intellij.python.processOutput.common.TraceContextKind
import com.intellij.python.processOutput.frontend.ProcessOutputTest
import com.intellij.python.processOutput.frontend.TreeFilter
import com.intellij.python.processOutput.frontend.finish
import com.intellij.python.processOutput.frontend.formatTime
import com.intellij.python.processOutput.frontend.ui.ProcessIsBackgroundKey
import com.intellij.python.processOutput.frontend.ui.ProcessIsErrorKey
import io.mockk.called
import io.mockk.verify
import kotlin.test.BeforeTest
@@ -50,8 +51,8 @@ internal class TreeSectionTest : ProcessOutputTest() {
@Test
fun `tree should display contexts and its children`() = processOutputTest {
val context = TraceContext("context")
val subcontext = TraceContext("subcontext")
val context = traceContext("context")
val subcontext = traceContext("subcontext")
// adding test processes
setTree {
@@ -164,7 +165,7 @@ internal class TreeSectionTest : ProcessOutputTest() {
}
// turn off display time filter
toggleTreeFilter(TreeFilter.ShowTime)
toggleTreeFilter(TreeFilter.Item.SHOW_TIME)
// should not display time when the filter is turned off
repeat(10) {
@@ -182,7 +183,10 @@ internal class TreeSectionTest : ProcessOutputTest() {
repeat(5) {
addProcess(
"background$it",
traceContext = NON_INTERACTIVE_ROOT_TRACE_CONTEXT,
traceContext = traceContext(
"non-interactive",
kind = TraceContextKind.NON_INTERACTIVE,
),
)
}
}
@@ -293,9 +297,25 @@ internal class TreeSectionTest : ProcessOutputTest() {
useUnmergedTree = true,
).performClick()
// background should have been called, but not time
verify(exactly = 1) { controllerSpy.toggleTreeFilter(TreeFilter.ShowBackgroundProcesses) }
verify(exactly = 0) { controllerSpy.toggleTreeFilter(TreeFilter.ShowTime) }
// background should have been called, but not time nor process weights
verify(exactly = 1) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_BACKGROUND_PROCESSES,
true,
)
}
verify(exactly = 0) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_PROCESS_WEIGHT,
any(),
)
}
verify(exactly = 0) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_TIME,
any(),
)
}
// clicking on time
onNodeWithTag(
@@ -303,8 +323,90 @@ internal class TreeSectionTest : ProcessOutputTest() {
useUnmergedTree = true,
).performClick()
// both filters should have been called
verify(exactly = 1) { controllerSpy.toggleTreeFilter(TreeFilter.ShowBackgroundProcesses) }
verify(exactly = 1) { controllerSpy.toggleTreeFilter(TreeFilter.ShowTime) }
// background and time should have been called, but not process weights
verify(exactly = 1) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_BACKGROUND_PROCESSES,
true,
)
}
verify(exactly = 0) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_PROCESS_WEIGHT,
any(),
)
}
verify(exactly = 1) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_TIME,
false,
)
}
// clicking on process weights
onNodeWithTag(
TreeSectionTestTags.FILTERS_PROCESS_WEIGHTS,
useUnmergedTree = true,
).performClick()
// all filters should have been called
verify(exactly = 1) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_BACKGROUND_PROCESSES,
true,
)
}
verify(exactly = 1) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_PROCESS_WEIGHT,
false,
)
}
verify(exactly = 1) {
controllerSpy.onTreeFilterItemToggled(
TreeFilter.Item.SHOW_TIME,
false,
)
}
}
@Test
fun `tree process items should display process weights when the filter is enabled`() =
processOutputTest {
// adding some processes
setTree {
repeat(3) {
addProcess("light$it", weight = ProcessWeightDto.LIGHT)
}
repeat(3) {
addProcess("medium$it", weight = ProcessWeightDto.MEDIUM)
}
repeat(3) {
addProcess("heavy$it", weight = ProcessWeightDto.HEAVY)
}
}
// should display medium and heavy icons
onAllNodesWithTag(
testTag = TreeSectionTestTags.PROCESS_ITEM_WEIGHT_MEDIUM_ICON,
useUnmergedTree = true,
).assertCountEquals(3)
onAllNodesWithTag(
testTag = TreeSectionTestTags.PROCESS_ITEM_WEIGHT_HEAVY_ICON,
useUnmergedTree = true,
).assertCountEquals(3)
// turn off process weight icons
toggleTreeFilter(TreeFilter.Item.SHOW_PROCESS_WEIGHT)
// should not display any weight icons
onAllNodesWithTag(
testTag = TreeSectionTestTags.PROCESS_ITEM_WEIGHT_MEDIUM_ICON,
useUnmergedTree = true,
).assertCountEquals(0)
onAllNodesWithTag(
testTag = TreeSectionTestTags.PROCESS_ITEM_WEIGHT_HEAVY_ICON,
useUnmergedTree = true,
).assertCountEquals(0)
}
}

View File

@@ -1,25 +1,26 @@
package com.intellij.python.processOutput.impl
package com.intellij.python.processOutput.frontend
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.snapshots.SnapshotStateSet
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import com.intellij.python.community.execService.impl.LoggedProcess
import com.intellij.python.community.execService.impl.LoggedProcessExe
import com.intellij.python.community.execService.impl.LoggedProcessExitInfo
import com.intellij.python.community.execService.impl.LoggedProcessLine
import com.intellij.python.community.execService.impl.LoggingLimits
import com.intellij.python.processOutput.impl.ui.toggle
import com.jetbrains.python.TraceContext
import com.intellij.python.processOutput.common.ExecutableDto
import com.intellij.python.processOutput.common.LoggedProcessDto
import com.intellij.python.processOutput.common.OutputKindDto
import com.intellij.python.processOutput.common.OutputLineDto
import com.intellij.python.processOutput.common.ProcessWeightDto
import com.intellij.python.processOutput.common.TraceContextDto
import com.intellij.python.processOutput.common.TraceContextKind
import com.intellij.python.processOutput.common.TraceContextUuid
import com.intellij.python.processOutput.frontend.ui.components.FilterActionGroupState
import com.intellij.python.processOutput.frontend.ui.toggle
import io.mockk.spyk
import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.runBlocking
@@ -35,14 +36,10 @@ import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
import org.junit.Rule
internal abstract class ProcessOutputTest {
protected val processTree = MutableStateFlow(buildTree<TreeNode> {})
protected val processTreeFilters: SnapshotStateSet<TreeFilter> = mutableStateSetOf(
TreeFilter.ShowTime,
)
private val traceContextCache = mutableMapOf<TraceContextUuid, TraceContextDto>()
protected val processTree = MutableStateFlow(buildTree<TreeNode> {})
protected val processOutputFilters: SnapshotStateSet<OutputFilter> = mutableStateSetOf(
OutputFilter.ShowTags,
)
protected val processOutputInfoExpanded = MutableStateFlow(false)
protected val processOutputOutputExpanded = MutableStateFlow(true)
@@ -50,7 +47,7 @@ internal abstract class ProcessOutputTest {
protected val testProcessTreeUiState: TreeUiState = run {
val selectableLazyListState = SelectableLazyListState(LazyListState())
TreeUiState(
filters = processTreeFilters,
filters = FilterActionGroupState(TreeFilter),
searchState = TextFieldState(),
selectableLazyListState = selectableLazyListState,
treeState = TreeState(selectableLazyListState),
@@ -58,7 +55,7 @@ internal abstract class ProcessOutputTest {
)
}
protected val testProcessOutputUiState: OutputUiState = OutputUiState(
filters = processOutputFilters,
filters = FilterActionGroupState(OutputFilter),
isInfoExpanded = processOutputInfoExpanded,
isOutputExpanded = processOutputOutputExpanded,
lazyListState = LazyListState(),
@@ -74,6 +71,9 @@ internal abstract class ProcessOutputTest {
override val processTreeUiState: TreeUiState = testProcessTreeUiState
override val processOutputUiState: OutputUiState = testProcessOutputUiState
override fun resolveTraceContext(uuid: TraceContextUuid): TraceContextDto? =
traceContextCache[uuid]
override fun collapseAllContexts() {
controllerSpy.collapseAllContexts()
}
@@ -86,12 +86,12 @@ internal abstract class ProcessOutputTest {
controllerSpy.selectProcess(process)
}
override fun toggleTreeFilter(filter: TreeFilter) {
controllerSpy.toggleTreeFilter(filter)
override fun onTreeFilterItemToggled(filterItem: TreeFilter.Item, enabled: Boolean) {
controllerSpy.onTreeFilterItemToggled(filterItem, enabled)
}
override fun toggleOutputFilter(filter: OutputFilter) {
controllerSpy.toggleOutputFilter(filter)
override fun onOutputFilterItemToggled(filterItem: OutputFilter.Item, enabled: Boolean) {
controllerSpy.onOutputFilterItemToggled(filterItem, enabled)
}
override fun toggleProcessInfo() {
@@ -113,14 +113,6 @@ internal abstract class ProcessOutputTest {
override fun copyOutputExitInfoToClipboard(loggedProcess: LoggedProcess) {
controllerSpy.copyOutputExitInfoToClipboard(loggedProcess)
}
override fun specifyAdditionalInfo(logId: Int, message: String?, isCritical: Boolean) {
controllerSpy.specifyAdditionalInfo(logId, message, isCritical)
}
override fun tryOpenLogInToolWindow(logId: Int): Boolean {
return controllerSpy.tryOpenLogInToolWindow(logId)
}
}
fun scaffoldTestContent(content: @Composable () -> Unit) {
@@ -133,10 +125,12 @@ internal abstract class ProcessOutputTest {
}
}
fun processOutputTest(body: suspend ComposeContentTestRule.() -> Unit) =
fun processOutputTest(body: suspend ComposeContentTestRule.() -> Unit) {
traceContextCache.clear()
runTest {
rule.body()
}
}
fun setTree(builder: suspend TreeBuilder<TreeNode>.() -> Unit) {
processTree.value = buildTree {
@@ -146,12 +140,12 @@ internal abstract class ProcessOutputTest {
}
}
fun expandContext(vararg traceContexts: TraceContext) {
fun expandContext(vararg traceContexts: TraceContextDto) {
testProcessTreeUiState.treeState.openNodes(traceContexts.toList())
}
fun toggleTreeFilter(filter: TreeFilter) {
processTreeFilters.toggle(filter)
fun toggleTreeFilter(filterItem: TreeFilter.Item) {
testProcessTreeUiState.filters.active.toggle(filterItem)
}
fun setSelectedProcess(process: LoggedProcess) {
@@ -166,54 +160,79 @@ internal abstract class ProcessOutputTest {
processOutputOutputExpanded.value = value
}
suspend fun process(
private val nextId = AtomicInteger(0)
fun process(
vararg command: String,
traceContext: TraceContext? = null,
traceContext: TraceContextDto? = null,
startedAt: Instant = Clock.System.now(),
cwd: String? = null,
lines: List<LoggedProcessLine> = listOf(),
exitInfo: LoggedProcessExitInfo? = null,
lines: List<OutputLineDto> = listOf(),
status: ProcessStatus = ProcessStatus.Running,
weight: ProcessWeightDto? = null,
): LoggedProcess =
LoggedProcess(
weight = null,
traceContext = traceContext ?: TraceContext("some title"),
pid = 123,
startedAt = startedAt,
cwd = cwd,
exe = LoggedProcessExe(
path = command.first(),
parts = command.first().split(Regex("[/\\\\]+")),
data = LoggedProcessDto(
weight = weight,
traceContextUuid =
traceContext?.uuid ?: traceContext("some title").uuid,
pid = 123,
startedAt = startedAt,
cwd = cwd,
exe =
ExecutableDto(
path = command.first(),
parts = command.first().split(Regex("[/\\\\]+")),
),
args = command.drop(1),
env = mapOf(),
target = "Local",
id = nextId.getAndAdd(1),
),
args = command.drop(1),
env = mapOf(),
target = "Local",
lines = run {
val flow = MutableSharedFlow<LoggedProcessLine>(replay = LoggingLimits.MAX_LINES)
lines.forEach { flow.emit(it) }
flow
},
exitInfo = MutableStateFlow(exitInfo),
lines = lines,
status = MutableStateFlow(status),
)
fun outLine(text: String): LoggedProcessLine =
LoggedProcessLine(
fun outLine(text: String, lineNo: Int): OutputLineDto =
OutputLineDto(
text = text,
kind = LoggedProcessLine.Kind.OUT,
kind = OutputKindDto.OUT,
lineNo = lineNo,
)
fun errLine(text: String): LoggedProcessLine =
LoggedProcessLine(
fun errLine(text: String, lineNo: Int): OutputLineDto =
OutputLineDto(
text = text,
kind = LoggedProcessLine.Kind.ERR,
kind = OutputKindDto.ERR,
lineNo = lineNo,
)
fun traceContext(
title: String,
kind: TraceContextKind = TraceContextKind.INTERACTIVE,
parent: TraceContextDto? = null,
): TraceContextDto {
val uuid = TraceContextUuid(UUID.randomUUID().toString())
val traceContext =
TraceContextDto(
title = title,
timestamp = Clock.System.now().toEpochMilliseconds(),
uuid = uuid,
kind = kind,
parentUuid = parent?.uuid,
)
traceContextCache[uuid] = traceContext
return traceContext
}
suspend fun TreeGeneratorScope<TreeNode>.addProcess(
vararg command: String,
traceContext: TraceContext? = null,
traceContext: TraceContextDto? = null,
startedAt: Instant = Clock.System.now(),
cwd: String? = null,
weight: ProcessWeightDto? = null,
) {
addProcess(
process(
@@ -221,6 +240,7 @@ internal abstract class ProcessOutputTest {
traceContext = traceContext,
startedAt = startedAt,
cwd = cwd,
weight = weight,
),
)
}
@@ -230,7 +250,7 @@ internal abstract class ProcessOutputTest {
}
fun TreeGeneratorScope<TreeNode>.addContext(
context: TraceContext,
context: TraceContextDto,
childrenGenerator: suspend ChildrenGeneratorScope<TreeNode>.() -> Unit,
) {
addNode(TreeNode.Context(context), context) {
@@ -242,12 +262,12 @@ internal abstract class ProcessOutputTest {
}
internal fun LoggedProcess.finish(
exitValue: Int,
exitCode: Int,
exitedAt: Instant = Clock.System.now(),
): LoggedProcess {
exitInfo.value = LoggedProcessExitInfo(
(status as MutableStateFlow<ProcessStatus>).value = ProcessStatus.Done(
exitedAt = exitedAt,
exitValue = exitValue,
exitCode = exitCode,
)
return this

View File

@@ -0,0 +1,30 @@
<idea-plugin visibility="internal">
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<module name="intellij.libraries.compose.foundation.desktop"/>
<module name="intellij.libraries.compose.runtime.desktop"/>
<module name="intellij.libraries.kotlin.reflect"/>
<module name="intellij.libraries.kotlinTestAssertionsCoreJvm"/>
<module name="intellij.libraries.kotlinx.coroutines.debug"/>
<module name="intellij.libraries.kotlinx.coroutines.test"/>
<module name="intellij.libraries.skiko"/>
<module name="intellij.platform.core"/>
<module name="intellij.platform.core.ui"/>
<module name="intellij.platform.ide"/>
<module name="intellij.platform.jewel.foundation"/>
<module name="intellij.platform.jewel.ideLafBridge"/>
<module name="intellij.platform.jewel.intUi.standalone"/>
<module name="intellij.platform.jewel.ui"/>
<module name="intellij.platform.projectModel"/>
<module name="intellij.platform.testFramework.common"/>
<module name="intellij.platform.testFramework.junit5"/>
<module name="intellij.platform.util.ui"/>
<module name="intellij.python.community"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.processOutput.common"/>
<module name="intellij.libraries.kotlinTest"/>
<module name="intellij.platform.frontend"/>
<module name="intellij.python.test.env.junit5"/>
</dependencies>
<!-- endregion -->
</idea-plugin>

View File

@@ -1,26 +0,0 @@
package com.intellij.python.processOutput.impl
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.python.processOutput.ProcessOutputApi
import org.jetbrains.annotations.Nls
internal class ProcessOutputApiImpl : ProcessOutputApi {
override fun specifyAdditionalInfo(
project: Project,
logId: Int,
message: @Nls String?,
isCritical: Boolean,
) {
val service = project.service<ProcessOutputControllerService>()
service.specifyAdditionalInfo(logId, message, isCritical)
}
override fun tryOpenLogInToolWindow(
project: Project,
logId: Int,
): Boolean {
val service = project.service<ProcessOutputControllerService>()
return service.tryOpenLogInToolWindow(logId)
}
}

View File

@@ -1,620 +0,0 @@
package com.intellij.python.processOutput.impl
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.SnapshotStateSet
import androidx.compose.ui.util.fastMaxOfOrDefault
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.python.community.execService.impl.ExecLoggerService
import com.intellij.python.community.execService.impl.LoggedProcess
import com.intellij.python.community.execService.impl.LoggedProcessLine
import com.intellij.python.processOutput.ProcessBinaryFileName
import com.intellij.python.processOutput.ProcessIcon
import com.intellij.python.processOutput.impl.ProcessOutputBundle.message
import com.intellij.python.processOutput.impl.ui.components.FilterItem
import com.intellij.python.processOutput.impl.ui.toggle
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT
import com.jetbrains.python.TraceContext
import java.util.WeakHashMap
import kotlin.collections.minus
import kotlin.collections.plus
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.jewel.foundation.lazy.SelectableLazyListState
import org.jetbrains.jewel.foundation.lazy.tree.Tree
import org.jetbrains.jewel.foundation.lazy.tree.TreeGeneratorScope
import org.jetbrains.jewel.foundation.lazy.tree.TreeState
import org.jetbrains.jewel.foundation.lazy.tree.buildTree
@ApiStatus.Internal
object CoroutineNames {
const val EXIT_INFO_COLLECTOR: String = "ProcessOutput.ExitInfoCollector"
}
internal object ProcessOutputControllerServiceLimits {
const val MAX_PROCESSES = 512
}
internal interface ProcessOutputController {
val selectedProcess: StateFlow<LoggedProcess?>
val processTreeUiState: TreeUiState
val processOutputUiState: OutputUiState
fun collapseAllContexts()
fun expandAllContexts()
fun selectProcess(process: LoggedProcess?)
fun toggleTreeFilter(filter: TreeFilter)
fun toggleOutputFilter(filter: OutputFilter)
fun toggleProcessInfo()
fun toggleProcessOutput()
fun specifyAdditionalInfo(logId: Int, message: @Nls String?, isCritical: Boolean)
fun copyOutputToClipboard(loggedProcess: LoggedProcess)
fun copyOutputTagAtIndexToClipboard(loggedProcess: LoggedProcess, fromIndex: Int)
fun copyOutputExitInfoToClipboard(loggedProcess: LoggedProcess)
@RequiresEdt
fun tryOpenLogInToolWindow(logId: Int): Boolean
}
@ApiStatus.Internal
data class TreeUiState(
val filters: Set<TreeFilter>,
val searchState: TextFieldState,
val selectableLazyListState: SelectableLazyListState,
val treeState: TreeState,
val tree: StateFlow<Tree<TreeNode>>,
)
@ApiStatus.Internal
sealed class TreeFilter : FilterItem {
object ShowTime : TreeFilter() {
override val title: String = message("process.output.filters.tree.time")
}
object ShowBackgroundProcesses : TreeFilter() {
override val title: String = message("process.output.filters.tree.backgroundProcesses")
}
}
@ApiStatus.Internal
sealed interface TreeNode {
data class Context(
val traceContext: TraceContext,
) : TreeNode
data class Process(
val process: LoggedProcess,
val icon: ProcessIcon?,
) : TreeNode
}
@ApiStatus.Internal
sealed class OutputFilter : FilterItem {
object ShowTags : OutputFilter() {
override val title: String = message("process.output.filters.output.tags")
}
}
@ApiStatus.Internal
data class OutputUiState(
val filters: Set<OutputFilter>,
val isInfoExpanded: StateFlow<Boolean>,
val isOutputExpanded: StateFlow<Boolean>,
val lazyListState: LazyListState,
)
@ApiStatus.Internal
@Service(Service.Level.PROJECT)
class ProcessOutputControllerService(
private val project: Project,
private val coroutineScope: CoroutineScope,
) : ProcessOutputController {
private val shouldScrollToTop = MutableStateFlow(false)
internal val loggedProcesses: StateFlow<List<LoggedProcess>> = run {
var processList = listOf<LoggedProcess>()
ApplicationManager.getApplication().service<ExecLoggerService>()
.processes
.map {
// If a new item was added while the process tree is fully scrolled to top, we need
// to manually scroll all the way to top once a new item is added to the list state,
// as it is not done automatically.
if (!processTreeUiState.treeState.canScrollBackward) {
shouldScrollToTop.value = true
}
processList = processList + it
if (processList.size > ProcessOutputControllerServiceLimits.MAX_PROCESSES) {
processList = processList.drop(
processList.size - ProcessOutputControllerServiceLimits.MAX_PROCESSES,
)
}
it.traceContext
?.takeIf { context -> context != NON_INTERACTIVE_ROOT_TRACE_CONTEXT }
?.also { context ->
processTreeUiState.treeState.openNodes(context.hierarchy())
}
processList
}
.stateIn(
coroutineScope,
SharingStarted.Eagerly,
emptyList(),
)
}
private val processTree = MutableStateFlow(buildTree<TreeNode> {})
private val processTreeFilters: SnapshotStateSet<TreeFilter> = mutableStateSetOf(
TreeFilter.ShowTime,
)
private val processOutputFilters: SnapshotStateSet<OutputFilter> = mutableStateSetOf(
OutputFilter.ShowTags,
)
private val processOutputInfoExpanded = MutableStateFlow(false)
private val processOutputOutputExpanded = MutableStateFlow(true)
override val selectedProcess: MutableStateFlow<LoggedProcess?> = MutableStateFlow(null)
override val processTreeUiState: TreeUiState = run {
val selectableLazyListState = SelectableLazyListState(LazyListState())
TreeUiState(
filters = processTreeFilters,
searchState = TextFieldState(),
selectableLazyListState = selectableLazyListState,
treeState = TreeState(selectableLazyListState),
tree = processTree,
)
}
override val processOutputUiState: OutputUiState = OutputUiState(
filters = processOutputFilters,
isInfoExpanded = processOutputInfoExpanded,
isOutputExpanded = processOutputOutputExpanded,
lazyListState = LazyListState(),
)
private val iconMapping = ProcessOutputIconMappingData.mapping
private val iconMatchers = ProcessOutputIconMappingData.matchers
private val iconCache = WeakHashMap<LoggedProcess, ProcessIcon>()
init {
collectSearchStats()
collectProcessTree()
ensureProcessTreeScroll()
}
override fun collapseAllContexts() {
processTreeUiState.treeState.openNodes = setOf()
ProcessOutputUsageCollector.treeCollapseAllClicked()
}
override fun expandAllContexts() {
processTreeUiState.treeState.openNodes =
processTreeUiState.tree.value
.walkDepthFirst()
.mapNotNull {
when (val data = it.data) {
is TreeNode.Context -> data.traceContext
is TreeNode.Process -> null
}
}
.toSet()
ProcessOutputUsageCollector.treeExpandAllClicked()
}
override fun selectProcess(process: LoggedProcess?) {
selectedProcess.value = process
ProcessOutputUsageCollector.treeProcessSelected()
}
override fun toggleTreeFilter(filter: TreeFilter) {
processTreeFilters.toggle(filter)
when (filter) {
TreeFilter.ShowBackgroundProcesses -> {
ProcessOutputUsageCollector.treeFilterBackgroundProcessesToggled(
processTreeFilters.contains(
TreeFilter.ShowBackgroundProcesses,
),
)
coroutineScope.launch(Dispatchers.EDT) {
processTreeUiState.selectableLazyListState.lazyListState.scrollToItem(0)
}
}
TreeFilter.ShowTime ->
ProcessOutputUsageCollector.treeFilterTimeToggled(
processTreeFilters.contains(TreeFilter.ShowTime),
)
}
}
override fun toggleOutputFilter(filter: OutputFilter) {
processOutputFilters.toggle(filter)
when (filter) {
OutputFilter.ShowTags ->
ProcessOutputUsageCollector.outputFilterShowTagsToggled(
processOutputFilters.contains(OutputFilter.ShowTags),
)
}
}
override fun toggleProcessInfo() {
val expanded = processOutputInfoExpanded.value
processOutputInfoExpanded.value = !expanded
ProcessOutputUsageCollector.outputProcessInfoToggled(!expanded)
}
override fun toggleProcessOutput() {
val expanded = processOutputOutputExpanded.value
processOutputOutputExpanded.value = !expanded
ProcessOutputUsageCollector.outputProcessOutputToggled(!expanded)
}
override fun copyOutputToClipboard(loggedProcess: LoggedProcess) {
val showTags = processOutputUiState.filters.contains(OutputFilter.ShowTags)
val stringToCopy = buildString {
loggedProcess.lines.replayCache.forEach { line ->
if (showTags) {
val tag = when (line.kind) {
LoggedProcessLine.Kind.ERR -> Tag.ERROR
LoggedProcessLine.Kind.OUT -> Tag.OUTPUT
}
append("[$tag] ".padStart(Tag.maxLength + 3))
} else {
repeat(Tag.maxLength + 3) {
append(' ')
}
}
appendLine(line.text)
}
loggedProcess.exitInfo.value?.also {
append("[${Tag.EXIT}] ".padStart(Tag.maxLength + 3))
append(it.exitValue)
it.additionalMessageToUser?.also { message ->
append(": ")
append(message)
}
appendLine()
}
}
CopyPasteManager.copyTextToClipboard(stringToCopy)
ProcessOutputUsageCollector.outputCopyClicked()
}
override fun copyOutputTagAtIndexToClipboard(
loggedProcess: LoggedProcess,
fromIndex: Int,
) {
val stringToCopy = buildString {
val replayCache = loggedProcess.lines.replayCache
replayCache
.drop(fromIndex)
.takeWhile { it.kind == replayCache[fromIndex].kind }
.forEach {
appendLine(it.text)
}
}
CopyPasteManager.copyTextToClipboard(stringToCopy)
ProcessOutputUsageCollector.outputTagSectionCopyClicked()
}
override fun copyOutputExitInfoToClipboard(loggedProcess: LoggedProcess) {
val exitInfo = loggedProcess.exitInfo.value ?: return
val stringToCopy = buildString {
append(exitInfo.exitValue)
exitInfo.additionalMessageToUser?.also { message ->
append(": ")
append(message)
}
appendLine()
}
CopyPasteManager.copyTextToClipboard(stringToCopy)
ProcessOutputUsageCollector.outputExitInfoCopyClicked()
}
@RequiresEdt
override fun tryOpenLogInToolWindow(logId: Int): Boolean {
val match = loggedProcesses.value.find { process -> process.id == logId }
if (match == null) {
return false
}
ToolWindowManager.getInstance(project).getToolWindow(TOOL_WINDOW_ID)?.show()
coroutineScope.launch(Dispatchers.EDT) {
val process = loggedProcesses.value.find { it.id == logId } ?: return@launch
// select the process
selectedProcess.value = process
// open all the parent nodes of the process
process.traceContext?.also {
processTreeUiState.treeState.openNodes(it.hierarchy())
}
// select the process in the list state
processTreeUiState.treeState.selectedKeys = setOf(process)
// scroll to the top of the list
processTreeUiState.selectableLazyListState.lazyListState.scrollToItem(0)
// clear search text
processTreeUiState.searchState.clearText()
// expand process output section
processOutputOutputExpanded.value = true
// wait until output has recomposed
delay(100.milliseconds)
// scroll output all the way to the bottom
val index = processOutputUiState.lazyListState.layoutInfo.totalItemsCount
processOutputUiState.lazyListState.scrollToItem(index.coerceAtLeast(0))
}
ProcessOutputUsageCollector.toolwindowOpenedDueToError()
return true
}
override fun specifyAdditionalInfo(logId: Int, @Nls message: String?, isCritical: Boolean) {
loggedProcesses.value.find { it.id == logId }?.exitInfo?.also { exitInfo ->
@Suppress("HardCodedStringLiteral")
exitInfo.value = exitInfo.value?.copy(
additionalMessageToUser = message?.trim(),
isCritical = isCritical,
)
}
}
private fun collectSearchStats() {
coroutineScope.launch {
snapshotFlow { processTreeUiState.searchState.text }
.collect {
ProcessOutputUsageCollector.treeSearchEdited()
}
}
}
@OptIn(FlowPreview::class)
private fun collectProcessTree() {
val backgroundErrorProcesses = MutableStateFlow<Set<Int>>(setOf())
val backgroundObservingCoroutines = mutableListOf<Job>()
coroutineScope.launch {
loggedProcesses
.debounce(100.milliseconds)
.collect { list ->
for (coroutine in backgroundObservingCoroutines) {
coroutine.cancelAndJoin()
}
backgroundObservingCoroutines.clear()
backgroundErrorProcesses.value = setOf()
list
.filter { it.traceContext == NON_INTERACTIVE_ROOT_TRACE_CONTEXT }
.forEach { process ->
val exitInfo = process.exitInfo.value
if (exitInfo != null) {
if (exitInfo.exitValue != 0) {
backgroundErrorProcesses.value += process.id
}
return@forEach
}
backgroundObservingCoroutines +=
launch(CoroutineName(CoroutineNames.EXIT_INFO_COLLECTOR)) {
process.exitInfo.collect {
val exitValue = it?.exitValue
if (exitValue != null && exitValue != 0) {
backgroundErrorProcesses.value += process.id
} else {
backgroundErrorProcesses.value -= process.id
}
}
}
}
}
}
combine(
backgroundErrorProcesses,
loggedProcesses.debounce(100.milliseconds),
snapshotFlow { processTreeUiState.searchState.text },
snapshotFlow { processTreeUiState.filters.toSet() },
)
{ backgroundErrorProcesses, processList, search, filters ->
val lowercaseSearch = search.toString().trim().lowercase()
var filteredProcesses =
processList
.reversed()
.filter {
it.shortenedCommandString
.lowercase()
.contains(lowercaseSearch)
}
if (!filters.contains(TreeFilter.ShowBackgroundProcesses)) {
filteredProcesses = filteredProcesses.filter {
it.traceContext != NON_INTERACTIVE_ROOT_TRACE_CONTEXT
|| backgroundErrorProcesses.contains(it.id)
}
}
data class Node(
val traceContext: TraceContext? = null,
val process: LoggedProcess? = null,
val children: MutableList<Node> = mutableListOf(),
)
val root = mutableListOf<Node>()
filteredProcesses.forEach { process ->
when (val traceContext = process.traceContext) {
null, NON_INTERACTIVE_ROOT_TRACE_CONTEXT -> root += Node(process = process)
else -> {
val hierarchy = traceContext.hierarchy()
var currentRoot = root
hierarchy.forEach { currentContext ->
val node =
currentRoot
.firstOrNull { node ->
node.traceContext == currentContext
}
?: Node(traceContext = currentContext).also {
currentRoot += it
}
currentRoot = node.children
}
currentRoot += Node(process = process)
}
}
}
fun TreeGeneratorScope<TreeNode>.buildNodeTree(root: List<Node>) {
root.forEach { (traceContext, process, children) ->
if (traceContext != null) {
addNode(TreeNode.Context(traceContext), traceContext) {
buildNodeTree(children)
}
} else if (process != null) {
addLeaf(
TreeNode.Process(process, resolveProcessIcon(process)),
process,
)
}
}
}
val newTree = buildTree {
buildNodeTree(root)
}
if (newTree.isEmpty()) {
selectProcess(null)
}
processTree.value = newTree
}.launchIn(coroutineScope)
}
private fun ensureProcessTreeScroll() {
coroutineScope.launch(Dispatchers.EDT) {
combine(
snapshotFlow { processTreeUiState.selectableLazyListState.firstVisibleItemIndex },
shouldScrollToTop,
) { firstVisibleIndex, processes -> (firstVisibleIndex > 0) to processes }
.collect { (canScrollBackwards, shouldScrollToTopValue) ->
if (canScrollBackwards && shouldScrollToTopValue) {
shouldScrollToTop.value = false
processTreeUiState.selectableLazyListState.scrollToItem(0)
}
}
}
}
private fun resolveProcessIcon(loggedProcess: LoggedProcess): ProcessIcon? {
iconCache[loggedProcess]?.also {
return it
}
val exe = loggedProcess.exe.parts.lastOrNull() ?: return null
val exeWithoutExt = exe.substringBeforeLast('.')
iconMapping[ProcessBinaryFileName(exeWithoutExt)]?.also {
iconCache[loggedProcess] = it
return it
}
for (matcher in iconMatchers) {
if (matcher.matcher(ProcessBinaryFileName(exeWithoutExt))) {
iconCache[loggedProcess] = matcher.icon
return matcher.icon
}
}
return null
}
}
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")
val maxLength: Int =
Tag::class.java.declaredFields
.filter { it.type == String::class.java }
.fastMaxOfOrDefault(0) { (it.get(null) as String).length }
}
private fun TraceContext.hierarchy(): List<TraceContext> {
val hierarchy = mutableListOf<TraceContext>()
var currentContext: TraceContext? = this
while (currentContext != null) {
hierarchy.add(0, currentContext)
currentContext = currentContext.parentTraceContext
}
return hierarchy
}

View File

@@ -1,11 +0,0 @@
<idea-plugin>
<dependencies>
<module name="intellij.python.processOutput.impl"/>
<module name="intellij.python.venv._test"/>
<module name="intellij.python.community.junit5Tests.framework._test"/>
<module name="intellij.python.test.env.junit5"/>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.libraries.kotlinTest"/>
<!-- endregion -->
</dependencies>
</idea-plugin>

View File

@@ -1,18 +0,0 @@
<idea-plugin>
<dependencies>
<module name="intellij.python.community"/>
<module name="intellij.python.sdk"/>
</dependencies>
<extensionPoints>
<extensionPoint
qualifiedName="com.intellij.python.processOutput.processOutputApi"
interface="com.intellij.python.processOutput.ProcessOutputApi"
/>
<extensionPoint
qualifiedName="com.intellij.python.processOutput.processOutputIconMapping"
interface="com.intellij.python.processOutput.ProcessOutputIconMapping"
/>
</extensionPoints>
</idea-plugin>

View File

@@ -1,18 +0,0 @@
package com.intellij.python.processOutput
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.Nls
interface ProcessOutputApi {
companion object {
private val EP_NAME = ExtensionPointName<ProcessOutputApi>("com.intellij.python.processOutput.processOutputApi")
fun getInstance(): ProcessOutputApi? =
EP_NAME.extensionList.firstOrNull()
}
fun specifyAdditionalInfo(project: Project, logId: Int, message: @Nls String?, isCritical: Boolean)
fun tryOpenLogInToolWindow(project: Project, logId: Int): Boolean
}

View File

@@ -17,7 +17,6 @@
<extensions defaultExtensionNs="com.intellij.platform">
<rpc.backend.remoteApiProvider implementation="com.intellij.python.sdkConfigurator.backend.impl.rpcBridge.ApiProvider"/>
</extensions>
<actions>
<action class="com.intellij.python.sdkConfigurator.backend.impl.platformBridge.ConfigureSDKAction" id="ConfigureSDKAction"/>
</actions>

View File

@@ -19,7 +19,7 @@ jvm_library(
"//platform/util",
"//python/openapi:community",
"//python/common",
"//python/python-process-output:processOutput",
"//python/python-process-output/common",
]
)
### auto-generated section `build intellij.python.community.impl.uv.common` end

View File

@@ -14,6 +14,6 @@
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.python.community" />
<orderEntry type="module" module-name="intellij.python.common" />
<orderEntry type="module" module-name="intellij.python.processOutput" />
<orderEntry type="module" module-name="intellij.python.processOutput.common" />
</component>
</module>

View File

@@ -2,11 +2,11 @@
<dependencies>
<module name="intellij.python.common"/>
<module name="intellij.python.community"/>
<module name="intellij.python.processOutput"/>
<module name="intellij.python.processOutput.common"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<python.common.toolToIconMapper implementation="com.intellij.python.community.impl.uv.common.impl.UvToolIdMapper"/>
<python.processOutput.processOutputIconMapping implementation="com.intellij.python.community.impl.uv.common.impl.UvIconMapping"/>
<python.processOutput.common.processOutputIconMapping implementation="com.intellij.python.community.impl.uv.common.impl.UvIconMapping"/>
</extensions>
</idea-plugin>

View File

@@ -2,9 +2,9 @@
package com.intellij.python.community.impl.uv.common.impl
import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons
import com.intellij.python.processOutput.ProcessBinaryFileName
import com.intellij.python.processOutput.ProcessIcon
import com.intellij.python.processOutput.ProcessOutputIconMapping
import com.intellij.python.processOutput.common.ProcessBinaryFileName
import com.intellij.python.processOutput.common.ProcessIcon
import com.intellij.python.processOutput.common.ProcessOutputIconMapping
internal class UvIconMapping : ProcessOutputIconMapping() {
override val mapping: Map<ProcessBinaryFileName, ProcessIcon> =

Some files were not shown because too many files have changed in this diff Show More