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
5
.idea/modules.xml
generated
@@ -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" />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -2,6 +2,7 @@
|
||||
<dependencies>
|
||||
<module name="intellij.python.community"/>
|
||||
<module name="intellij.python.community.helpersLocator"/>
|
||||
<module name="intellij.python.processOutput.common"/>
|
||||
</dependencies>
|
||||
|
||||
<extensionPoints>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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> =
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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> =
|
||||
|
||||
@@ -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
|
||||
23
python/python-process-output/backend/BUILD.bazel
Normal 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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
25
python/python-process-output/backend/src/ApiProvider.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
41
python/python-process-output/common/BUILD.bazel
Normal 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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.intellij.python.processOutput
|
||||
package com.intellij.python.processOutput.common
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
131
python/python-process-output/common/src/backendApi.kt
Normal 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>())
|
||||
131
python/python-process-output/common/src/processEventDto.kt
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" />
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 360 B After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 360 B After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 548 B After Width: | Height: | Size: 548 B |
|
Before Width: | Height: | Size: 548 B After Width: | Height: | Size: 548 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 646 B |
|
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 646 B |
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -0,0 +1,4 @@
|
||||
@ApiStatus.Internal
|
||||
package com.intellij.python.processOutput.frontend;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.intellij.python.processOutput.impl.ui
|
||||
package com.intellij.python.processOutput.frontend.ui
|
||||
|
||||
import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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(" ")
|
||||
@@ -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
|
||||
178
python/python-process-output/frontend/test/junit5Tests/env/LoggingTest.kt
vendored
Normal 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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
@@ -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 {
|
||||
@@ -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() {
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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> =
|
||||
|
||||