Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## [Unreleased]

### Fixed

- The channel console no longer throws an EDT-threading error while streaming live output into an aggregate tab.
- The report buttons no longer trigger a "slow operations on EDT" error: report paths now resolve off the UI thread.

## [2026.5.262] - 2026-08-12

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1073,13 +1073,18 @@ object TestoChannelsUi {

// Prefix the aggregate's per-test header (a hyperlink to the test) with the Testo icon. Console output is
// buffered, so the editor offset only resolves once it is flushed β€” hence performWhenNoDeferredOutput.
// Live chunks arrive off the EDT (test-reader thread), but performWhenNoDeferredOutput asserts EDT β€” hop first.
private fun addTestoIconInlay(view: ConsoleViewImpl, offset: Int) {
view.performWhenNoDeferredOutput {
val editor = view.editor as? EditorEx ?: return@performWhenNoDeferredOutput
if (offset in 0..editor.document.textLength) {
editor.inlayModel.addInlineElement(offset, false, TestoIconInlayRenderer)
val app = ApplicationManager.getApplication()
val task = Runnable {
view.performWhenNoDeferredOutput {
val editor = view.editor as? EditorEx ?: return@performWhenNoDeferredOutput
if (offset in 0..editor.document.textLength) {
editor.inlayModel.addInlineElement(offset, false, TestoIconInlayRenderer)
}
}
}
if (app.isDispatchThread) task.run() else app.invokeLater(task)
}

private fun ensureInstalled(): JBEditorTabs? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.RightAlignedToolbarAction
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.actionSystem.ex.CustomComponentAction
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
Expand Down Expand Up @@ -150,6 +152,7 @@ class TestoReportsAction(
// A fresh cell has no tooltip yet, so the first refresh must go through however little has changed.
private var refreshed = false
private var hovered = false
private var resolving = false

// Asked for per paint: a font set once on a raw JComponent outlives a zoom (no UI delegate reinstalls it).
override fun getFont(): Font = UIUtil.getLabelFont()
Expand Down Expand Up @@ -186,7 +189,31 @@ class TestoReportsAction(
// Not while the run is going: a report is announced as Testo starts writing it, over the path the
// previous run wrote to β€” a check now would offer that run's file.
val finished = reports.runFinished
val found = if (finished) resolveReport(ref, project, mapToLocal, reports.runStartedAt) else null
if (!finished) {
applyResolved(null, false)
return
}
// resolveReport goes through the PHP path mapper, whose getLocalPath hits the file index β€” a slow operation
// forbidden on the EDT, and this runs off a Swing timer on the EDT.
if (resolving) return
resolving = true
val startedAt = reports.runStartedAt
val cellRef = ref
ApplicationManager.getApplication().executeOnPooledThread {
val found = resolveReport(cellRef, project, mapToLocal, startedAt)
ApplicationManager.getApplication().invokeLater(
{
resolving = false
// A rerun may have started while this resolved; applying then would auto-open the previous
// run's report and mark the new run as already opened. Drop it β€” the next tick sees the run.
if (reports.runStartedAt == startedAt && reports.runFinished) applyResolved(found, true)
},
ModalityState.any(),
) { project.isDisposed }
}
}

private fun applyResolved(found: Path?, finished: Boolean) {
maybeAutoOpen(found, finished)
val willOpen = !finished && TestoReportAutoOpen.decide(project, reports, ref).isNotEmpty()
// Tooltip and repaint only on a real change: this runs twice a second.
Expand Down
Loading