This document provides detailed information about the Structured Coroutines IntelliJ/Android Studio plugin.
- Installation
- Features
- Inspections
- Structured Coroutines Tool Window
- Quick Fixes
- Intentions
- Gutter Icons
- Configuration
- Building from Source
- K2 Mode Compatibility
- Troubleshooting
- Open IntelliJ IDEA or Android Studio
- Go to Settings/Preferences > Plugins
- Search for "Structured Coroutines"
- Click Install
- Restart the IDE
- Download the plugin ZIP file from Releases
- Go to Settings/Preferences > Plugins
- Click the gear icon and select Install Plugin from Disk...
- Select the downloaded ZIP file
- Restart the IDE
# Clone the repository
git clone https://github.com/santimattius/structured-coroutines.git
cd structured-coroutines
# Build the plugin ZIP (IntelliJ Platform Gradle Plugin 2.x)
./gradlew :intellij-plugin:buildPlugin
# The plugin ZIP will be at:
# intellij-plugin/build/distributions/intellij-plugin-<version>.zipThen install from disk: Settings/Preferences → Plugins → gear icon → Install Plugin from Disk... → select the ZIP from intellij-plugin/build/distributions/.
The plugin provides four main feature categories:
- Real-time Inspections - Detect coroutine anti-patterns as you type
- Structured Coroutines Tool Window - View all findings for the current file in one place
- Quick Fixes - One-click corrections for detected issues
- Intentions - Refactoring suggestions available via Alt+Enter
- Gutter Icons - Visual indicators for scope type and dispatcher context
v1.0.0: 35 inspections aligned with
docs/rule-codes.yml.
Includes Flow Chain Analyzer intention, Compose/KMP/interop rules from v0.8–v1.0.
| Inspection | Severity | Description |
|---|---|---|
| GlobalScopeUsage | ERROR | Detects GlobalScope.launch/async |
| MainDispatcherMisuse | WARNING | Detects blocking code on Dispatchers.Main |
| ScopeReuseAfterCancel | WARNING | Detects scope cancelled then reused |
| RunBlockingInSuspend | ERROR | Detects runBlocking in suspend functions |
| UnstructuredLaunch | WARNING | Detects launch without structured scope (recognizes @StructuredScope on parameters and properties) |
| AsyncWithoutAwait | WARNING | Detects async without await() |
| InlineCoroutineScope | ERROR | Detects CoroutineScope(...).launch |
| JobInBuilderContext | ERROR | Detects Job()/SupervisorJob() in builders |
| SuspendInFinally | WARNING | Detects suspend calls in finally without NonCancellable |
| CancellationExceptionSwallowed | WARNING | Detects catch(Exception) swallowing cancellation |
| DispatchersUnconfined | WARNING | Detects Dispatchers.Unconfined usage |
| LifecycleAwareFlowCollection | WARNING | Flow collect in lifecycleScope without repeatOnLifecycle/flowWithLifecycle (§8.2 ARCH_002) |
| LoopWithoutYield | WARNING | Detects loops in suspend functions without cooperation points |
Problem: GlobalScope bypasses structured concurrency, leading to resource leaks.
// ❌ BAD
GlobalScope.launch {
fetchData() // Runs until completion regardless of lifecycle
}
// ✅ GOOD
viewModelScope.launch {
fetchData() // Cancelled when ViewModel is cleared
}Problem: Blocking calls on Dispatchers.Main can cause ANRs (Android) or UI freezes.
// ❌ BAD
withContext(Dispatchers.Main) {
Thread.sleep(1000) // Blocks UI thread!
}
// ✅ GOOD
withContext(Dispatchers.IO) {
Thread.sleep(1000) // Safe - IO thread pool
}Problem: A cancelled scope cannot launch new coroutines.
// ❌ BAD
fun process(scope: CoroutineScope) {
scope.cancel()
scope.launch { work() } // Silently fails!
}
// ✅ GOOD
fun process(scope: CoroutineScope) {
scope.coroutineContext.job.cancelChildren()
scope.launch { work() } // Works - scope is still active
}Problem: runBlocking in suspend functions blocks the thread, defeating coroutines.
// ❌ BAD
suspend fun fetchData() {
runBlocking { // Blocks the thread!
delay(1000)
}
}
// ✅ GOOD
suspend fun fetchData() {
delay(1000) // Suspends without blocking
}Problem: async creates a Deferred that should be awaited.
// ❌ BAD - Result never used
scope.async {
computeValue()
}
// ✅ GOOD - Use the result
val result = scope.async { computeValue() }.await()
// ✅ GOOD - Use launch if result not needed
scope.launch {
computeValue()
}Problem: Job()/SupervisorJob() in builders breaks cancellation hierarchy.
// ❌ BAD
scope.launch(Job()) {
work() // Won't be cancelled with parent!
}
// ✅ GOOD
supervisorScope {
launch { work() } // Proper structured concurrency
}Problem: Suspend calls in finally may not complete if coroutine is cancelled.
// ❌ BAD
try { work() } finally {
saveToDb() // May not complete!
}
// ✅ GOOD
try { work() } finally {
withContext(NonCancellable) {
saveToDb() // Guaranteed to complete
}
}Problem: Catching generic Exception swallows CancellationException.
// ❌ BAD
suspend fun work() {
try { fetchData() }
catch (e: Exception) { log(e) } // Breaks cancellation!
}
// ✅ GOOD
suspend fun work() {
try { fetchData() }
catch (e: CancellationException) { throw e }
catch (e: Exception) { log(e) }
}The plugin adds a Structured Coroutines tool window (bottom strip) that shows all inspection findings for the current file.
- View → Tool Windows → Structured Coroutines
- Open a Kotlin file in the editor (the one you want to analyze).
- Click Refresh in the tool window toolbar. The plugin runs all Structured Coroutines inspections on that file (single source of truth:
StructuredCoroutinesInspectionProvider). - The table shows Severity (icon), Location (file:line), Inspection name, and Message.
- Double-click a row to navigate to the reported element in the editor.
If no file is selected or the current file is not Kotlin, the view shows a short message. With the tool window you can see every coroutine issue in the active file in one place without scrolling through the editor.
Each inspection provides one or more quick fixes accessible via Alt+Enter (or the lightbulb icon):
| Quick Fix | Applies To |
|---|---|
| Replace GlobalScope with viewModelScope | GlobalScopeUsage |
| Replace GlobalScope with lifecycleScope | GlobalScopeUsage |
| Replace GlobalScope with coroutineScope { } | GlobalScopeUsage |
| Wrap with withContext(Dispatchers.IO) | MainDispatcherMisuse |
| Replace cancel() with cancelChildren() | ScopeReuseAfterCancel |
| Remove runBlocking | RunBlockingInSuspend |
| Add .await() | AsyncWithoutAwait |
| Convert async to launch | AsyncWithoutAwait |
| Wrap with withContext(NonCancellable) | SuspendInFinally |
| Add CancellationException catch clause | CancellationExceptionSwallowed |
| Replace with supervisorScope { } | JobInBuilderContext |
| Add cooperation point in loop (ensureActive / yield / delay(0)) | LoopWithoutYield (CANCEL_001) |
| Change superclass from CancellationException to Exception | CancellationExceptionSubclass (EXCEPT_002) |
Intentions are available via Alt+Enter when the cursor is on relevant code:
Availability: Inside a ViewModel class, on a launch/async call
Converts any coroutine launch to use viewModelScope:
// Before
someScope.launch { work() }
// After
viewModelScope.launch { work() }Availability: Inside Activity/Fragment, on a launch/async call
Converts any coroutine launch to use lifecycleScope:
// Before
someScope.launch { work() }
// After
lifecycleScope.launch { work() }Availability: Inside a suspend function
Wraps the function body with coroutineScope builder:
// Before
suspend fun process() {
launch { task1() }
launch { task2() }
}
// After
suspend fun process() = coroutineScope {
launch { task1() }
launch { task2() }
}Availability: On any launch call
Converts launch to async for returning a Deferred:
// Before
scope.launch { work() }
// After
scope.async { work() }Availability: Inside a coroutine builder lambda
Extracts the lambda body into a separate suspend function:
// Before
scope.launch {
val data = fetchData()
processData(data)
saveResult()
}
// After
scope.launch { performWork() }
private suspend fun performWork() {
val data = fetchData()
processData(data)
saveResult()
}Availability: Cursor inside a runBlocking { } call whose body contains delay()
Replaces runBlocking with runTest so tests use virtual time (kotlinx-coroutines-test) instead of real delays:
// Before
@Test
fun test() = runBlocking {
delay(1000)
assertEquals(1, result)
}
// After
@Test
fun test() = runTest {
delay(1000) // Virtual time - instant
assertEquals(1, result)
}Requires dependency: org.jetbrains.kotlinx:kotlinx-coroutines-test.
The plugin shows colored dots in the gutter to indicate scope types:
| Color | Scope Type | Safety |
|---|---|---|
| 🟢 Green | viewModelScope | Safe - tied to ViewModel lifecycle |
| 🔵 Blue | lifecycleScope | Safe - tied to lifecycle |
| 🟣 Purple | coroutineScope/supervisorScope | Safe - structured builder |
| ⚪ Gray | Custom scope | Depends on implementation |
| 🔴 Red | GlobalScope | Unsafe - no lifecycle |
Letter badges indicate the dispatcher:
| Icon | Dispatcher | Thread |
|---|---|---|
| M (Orange) | Dispatchers.Main | UI thread |
| I (Blue) | Dispatchers.IO | I/O thread pool |
| D (Green) | Dispatchers.Default | CPU thread pool |
| U (Red) | Dispatchers.Unconfined | Unpredictable |
- Go to Settings/Preferences > Editor > Inspections
- Navigate to Kotlin > Coroutines
- Check/uncheck individual inspections
- Adjust severity levels as needed (Error, Warning, Weak Warning)
You can create custom inspection profiles:
- Go to Settings/Preferences > Editor > Inspections
- Click the gear icon next to the profile dropdown
- Select Duplicate to create a copy
- Customize the new profile
To suppress an inspection for specific code:
// Suppress for a statement
@Suppress("GlobalScopeUsage")
GlobalScope.launch { work() }
// Suppress for a function
@Suppress("RunBlockingInSuspend")
suspend fun legacy() {
runBlocking { /* ... */ }
}
// Suppress for a file
@file:Suppress("GlobalScopeUsage")- JDK 21+ (required for IntelliJ Platform 2024.3+)
- Gradle 8.0+
# Build the plugin ZIP for local install or distribution
./gradlew :intellij-plugin:buildPlugin
# Build and run tests
./gradlew :intellij-plugin:build
# Run tests
./gradlew :intellij-plugin:test
# Verify plugin compatibility
./gradlew :intellij-plugin:verifyPlugin
# Run IDE sandbox for testing
./gradlew :intellij-plugin:runIdeintellij-plugin/
├── build.gradle.kts
├── src/main/kotlin/
│ └── io/github/santimattius/structured/intellij/
│ ├── StructuredCoroutinesBundle.kt
│ ├── inspections/
│ ├── quickfixes/
│ ├── intentions/
│ ├── guttericons/
│ ├── utils/
│ └── view/ # Tool window (panel, runner, tree visitor)
├── src/main/resources/
│ ├── META-INF/plugin.xml
│ └── messages/StructuredCoroutinesBundle.properties
└── src/test/kotlin/
The plugin fully supports the Kotlin K2 compiler mode, which is the new Kotlin compiler frontend used in recent versions of IntelliJ IDEA and Android Studio.
K2 is Kotlin's new compiler frontend that provides:
- Faster compilation times
- Better IDE performance
- Improved type inference
- More accurate code analysis
Android Studio (starting with Ladybug) and IntelliJ IDEA (2024.2+) use K2 mode by default for Kotlin code analysis.
The plugin declares K2 support via the supportsKotlinPluginMode extension in plugin.xml:
<extensions defaultExtensionNs="org.jetbrains.kotlin">
<supportsKotlinPluginMode supportsK2="true" />
</extensions>For K2 compatibility, the plugin implements specific patterns:
Line markers (gutter icons) must work with leaf PSI elements in K2 mode. The implementation uses:
override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
// Work with leaf elements only for K2 compatibility
if (element !is LeafPsiElement) return null
if (element.elementType != KtTokens.IDENTIFIER) return null
// Get parent KtNameReferenceExpression from the leaf
val nameRef = element.parent as? KtNameReferenceExpression ?: return null
// Continue with analysis...
}The plugin uses PSI-based analysis (not descriptor-based), which works identically in both K1 and K2 modes:
KtCallExpressionfor call detectionKtNameReferenceExpressionfor identifier analysisKtVisitorVoidfor tree traversal- Standard PSI utilities (
PsiTreeUtil,getParentOfType, etc.)
To verify the plugin works in K2 mode:
-
Check K2 Mode is Enabled:
- IntelliJ: Settings > Languages & Frameworks > Kotlin > Enable K2 Kotlin Mode
- Android Studio: K2 is enabled by default in Ladybug+
-
Test Plugin Features:
- Open a Kotlin file with coroutine code
- Verify gutter icons appear for
launch/asynccalls - Verify inspections highlight problematic patterns
- Verify quick fixes work correctly
-
Check Logs for Errors:
- Help > Show Log in Explorer/Finder
- Search for plugin-related errors
The plugin implements K2 compatibility through:
- Proper
supportsKotlinPluginModedeclaration inplugin.xml - Leaf element handling in line marker providers
- PSI-only analysis (no K1-specific descriptor APIs)
- Check IDE compatibility (requires IntelliJ 2024.3+)
- Verify Kotlin plugin is installed and enabled
- Check Help > Show Log for errors
- Ensure inspections are enabled in Settings
- Check if the file is in a source root
- Verify Kotlin plugin is working (try a basic Kotlin inspection)
- If using K2 mode, ensure you have the latest plugin version with K2 support
If gutter icons don't appear in K2 mode:
- Verify K2 support: Ensure the latest plugin version is installed
- Restart IDE: After enabling/disabling K2 mode, restart the IDE
- Invalidate caches: File > Invalidate Caches > Invalidate and Restart
- Check file type: Gutter icons only appear in
.ktfiles with coroutine code - Verify code patterns: Icons appear on
launch,async,withContextcalls
If the plugin causes slowdowns:
- Disable unused inspections
- Exclude large directories from inspection
- Report issues with profiler data
Please report issues at: https://github.com/santimattius/structured-coroutines/issues
Include:
- IDE version
- Plugin version
- Kotlin version
- Minimal code sample reproducing the issue
- Stack trace (if applicable)
| IDE | Minimum Version | Status |
|---|---|---|
| IntelliJ IDEA Community | 2024.3 | ✅ Supported |
| IntelliJ IDEA Ultimate | 2024.3 | ✅ Supported |
| Android Studio | Ladybug (2024.2) | ✅ Supported |
| Mode | Status | Notes |
|---|---|---|
| K1 (Classic) | ✅ Supported | Full functionality |
| K2 (New) | ✅ Supported | Full functionality |
| Feature | K1 Mode | K2 Mode |
|---|---|---|
| Inspections | ✅ | ✅ |
| Quick Fixes | ✅ | ✅ |
| Intentions | ✅ | ✅ |
| Gutter Icons (Scope) | ✅ | ✅ |
| Gutter Icons (Dispatcher) | ✅ | ✅ |
- Structured Coroutines tool window — View → Tool Windows → Structured Coroutines; lists all findings for the current file; Refresh to run inspections, double-click to navigate.
- @StructuredScope detection — UnstructuredLaunch inspection now correctly recognizes parameters and properties annotated with
@StructuredScope(viaScopeAnalyzer.findScopeDeclarationByNameandhasStructuredScopeAnnotation).
Initial Release
- 13 inspections for coroutine best practices (Loop without yield, Scope reuse, runBlocking, etc.)
- 9 quick fixes for automatic corrections
- 5 intentions for refactoring
- 2 gutter icon providers (scope type and dispatcher context)
- Full K2 compiler mode support
Copyright 2026 Santiago Mattiauda
Licensed under the Apache License, Version 2.0