Skip to content

Commit 14acd91

Browse files
committed
Fix UI freezes, SlowOperations on EDT, and write-unsafe context issues
These issues were happening in the project creator, both from loading the initial wizard, as well as during actual project creation.
1 parent 3bdbaaf commit 14acd91

File tree

8 files changed

+112
-48
lines changed

8 files changed

+112
-48
lines changed

src/main/kotlin/creator/JdkComboBoxWithPreference.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package com.demonwav.mcdev.creator
2222

23+
import com.demonwav.mcdev.util.invokeLater
2324
import com.intellij.ide.util.PropertiesComponent
2425
import com.intellij.ide.util.projectWizard.ProjectWizardUtil
2526
import com.intellij.ide.util.projectWizard.WizardContext
@@ -42,6 +43,7 @@ import com.intellij.openapi.util.Disposer
4243
import com.intellij.ui.dsl.builder.Cell
4344
import com.intellij.ui.dsl.builder.Row
4445
import javax.swing.JComponent
46+
import org.jetbrains.concurrency.runAsync
4547

4648
internal class JdkPreferenceData(
4749
var jdk: JavaSdkVersion,
@@ -89,15 +91,17 @@ class JdkComboBoxWithPreference internal constructor(
8991
preferenceData.jdk = version
9092
reloadModel()
9193

92-
for (jdkVersion in version.ordinal until JavaSdkVersion.values().size) {
93-
val jdk = JavaSdkVersion.values()[jdkVersion]
94+
for (jdkVersion in version.ordinal until JavaSdkVersion.entries.size) {
95+
val jdk = JavaSdkVersion.entries[jdkVersion]
9496

9597
val preferredSdkPath = preferenceData.sdkPathByJdk[jdk]
9698
if (preferredSdkPath != null) {
9799
val sdk = model.sdks.firstOrNull { it.homePath == preferredSdkPath }
98100
?: suggestions.firstOrNull { it.homePath == preferredSdkPath }
99101
if (sdk != null) {
100-
setSelectedItem(sdk)
102+
runAsync {
103+
setSelectedItem(sdk)
104+
}
101105
return
102106
}
103107
}
@@ -145,7 +149,7 @@ fun Row.jdkComboBoxWithPreference(
145149
for (preferenceDataStr in preferenceDataStrs) {
146150
val parts = preferenceDataStr.split('=', limit = 2)
147151
val featureVersion = parts.firstOrNull()?.toIntOrNull() ?: continue
148-
val knownJdkVersions = JavaSdkVersion.values()
152+
val knownJdkVersions = JavaSdkVersion.entries
149153
if (featureVersion !in knownJdkVersions.indices) {
150154
continue
151155
}
@@ -176,7 +180,9 @@ fun Row.jdkComboBoxWithPreference(
176180
}
177181

178182
val lastUsedSdk = stateComponent.getValue(selectedJdkProperty)
179-
ProjectWizardUtil.preselectJdkForNewModule(project, lastUsedSdk, comboBox) { true }
183+
runAsync {
184+
ProjectWizardUtil.preselectJdkForNewModule(project, lastUsedSdk, comboBox) { true }
185+
}
180186

181187
val windowChild = context.getUserData(AbstractWizard.KEY)!!.contentPanel
182188
comboBox.loadSuggestions(windowChild, context.disposable)

src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.intellij.codeInsight.actions.ReformatCodeProcessor
3434
import com.intellij.ide.projectView.ProjectView
3535
import com.intellij.ide.util.projectWizard.WizardContext
3636
import com.intellij.openapi.application.WriteAction
37+
import com.intellij.openapi.concurrency.awaitPromise
3738
import com.intellij.openapi.diagnostic.Attachment
3839
import com.intellij.openapi.diagnostic.ControlFlowException
3940
import com.intellij.openapi.diagnostic.thisLogger
@@ -53,11 +54,15 @@ import com.intellij.ui.dsl.builder.Panel
5354
import com.intellij.ui.dsl.builder.panel
5455
import com.intellij.util.application
5556
import java.nio.file.Path
57+
import java.time.Duration
58+
import java.util.concurrent.TimeUnit
5659
import java.util.function.Consumer
5760
import kotlin.io.path.createDirectories
5861
import kotlin.io.path.writeText
5962
import kotlinx.coroutines.CoroutineScope
6063
import kotlinx.coroutines.launch
64+
import org.jetbrains.concurrency.await
65+
import org.jetbrains.concurrency.runAsync
6166

6267
interface ExternalTemplatePropertyProvider {
6368

@@ -268,7 +273,11 @@ class CreatorTemplateProcessor(
268273
destPath.parent.createDirectories()
269274
destPath.writeText(processedContent)
270275

271-
val virtualFile = destPath.refreshAndFindVirtualFile()
276+
val virtualFile = runCatching {
277+
runAsync {
278+
destPath.refreshAndFindVirtualFile()
279+
}.blockingGet(20, TimeUnit.MILLISECONDS)
280+
}.getOrNull()
272281
if (virtualFile != null) {
273282
generatedFiles.add(file to virtualFile)
274283
} else {

src/main/kotlin/creator/custom/CustomPlatformStep.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate
2727
import com.demonwav.mcdev.creator.custom.providers.TemplateProvider
2828
import com.demonwav.mcdev.creator.modalityState
2929
import com.demonwav.mcdev.util.getOrLogException
30+
import com.demonwav.mcdev.util.invokeAndWait
31+
import com.demonwav.mcdev.util.runWriteTask
32+
import com.demonwav.mcdev.util.tryWriteSafeContext
3033
import com.intellij.ide.wizard.AbstractNewProjectWizardStep
3134
import com.intellij.ide.wizard.GitNewProjectWizardData
3235
import com.intellij.ide.wizard.NewProjectWizardBaseData
3336
import com.intellij.ide.wizard.NewProjectWizardStep
3437
import com.intellij.openapi.application.EDT
38+
import com.intellij.openapi.application.ModalityState
39+
import com.intellij.openapi.application.TransactionGuard
3540
import com.intellij.openapi.application.asContextElement
41+
import com.intellij.openapi.application.impl.ModalityStateEx
3642
import com.intellij.openapi.application.runWriteAction
3743
import com.intellij.openapi.diagnostic.logger
3844
import com.intellij.openapi.observable.properties.GraphProperty
@@ -51,10 +57,12 @@ import com.intellij.ui.dsl.builder.bindText
5157
import com.intellij.util.application
5258
import com.intellij.util.ui.AsyncProcessIcon
5359
import javax.swing.JLabel
60+
import javax.swing.SwingUtilities
5461
import kotlinx.coroutines.Dispatchers
5562
import kotlinx.coroutines.Job
5663
import kotlinx.coroutines.cancel
5764
import kotlinx.coroutines.launch
65+
import org.jetbrains.kotlin.descriptors.Modality
5866

5967
/**
6068
* The step to select a custom template repo.
@@ -248,11 +256,9 @@ class CustomPlatformStep(
248256
templateLoadingTextProperty.set(MCDevBundle("creator.step.generic.load_template.message"))
249257
templateLoadingProperty.set(true)
250258

251-
// For some reason syncRefresh doesn't play nice with writeAction() coroutines so we do it beforehand
252-
application.invokeAndWait(
253-
{ runWriteAction { VirtualFileManager.getInstance().syncRefresh() } },
254-
context.modalityState
255-
)
259+
tryWriteSafeContext(context.modalityState) {
260+
VirtualFileManager.getInstance().syncRefresh()
261+
}
256262

257263
val dialogCoroutineContext = context.modalityState.asContextElement()
258264
val uiContext = dialogCoroutineContext + Dispatchers.EDT

src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,18 @@ import com.demonwav.mcdev.creator.custom.TemplateDescriptor
2828
import com.demonwav.mcdev.creator.modalityState
2929
import com.demonwav.mcdev.creator.selectProxy
3030
import com.demonwav.mcdev.update.PluginUtil
31+
import com.demonwav.mcdev.util.asyncIO
3132
import com.demonwav.mcdev.util.capitalize
33+
import com.demonwav.mcdev.util.invokeAndWait
3234
import com.demonwav.mcdev.util.refreshSync
3335
import com.github.kittinunf.fuel.core.FuelManager
3436
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResult
3537
import com.github.kittinunf.result.getOrNull
3638
import com.github.kittinunf.result.onError
3739
import com.intellij.ide.util.projectWizard.WizardContext
3840
import com.intellij.openapi.application.PathManager
41+
import com.intellij.openapi.application.readAction
42+
import com.intellij.openapi.application.writeAction
3943
import com.intellij.openapi.diagnostic.ControlFlowException
4044
import com.intellij.openapi.diagnostic.thisLogger
4145
import com.intellij.openapi.observable.properties.PropertyGraph
@@ -44,6 +48,7 @@ import com.intellij.openapi.observable.util.transform
4448
import com.intellij.openapi.observable.util.trim
4549
import com.intellij.openapi.progress.ProgressIndicator
4650
import com.intellij.openapi.progress.ProgressManager
51+
import com.intellij.openapi.progress.blockingContext
4752
import com.intellij.openapi.util.NlsContexts
4853
import com.intellij.openapi.vfs.JarFileSystem
4954
import com.intellij.ui.CollectionComboBoxModel
@@ -57,16 +62,21 @@ import com.intellij.ui.dsl.builder.bindText
5762
import com.intellij.ui.dsl.builder.columns
5863
import com.intellij.ui.dsl.builder.panel
5964
import com.intellij.ui.dsl.builder.textValidation
65+
import com.intellij.util.application
6066
import com.intellij.util.io.createDirectories
6167
import java.awt.Component
6268
import java.nio.file.Path
6369
import javax.swing.JComponent
6470
import javax.swing.JLabel
6571
import javax.swing.JList
6672
import javax.swing.ListCellRenderer
73+
import javax.swing.SwingUtilities
6774
import kotlin.io.path.absolutePathString
6875
import kotlin.io.path.exists
6976
import kotlin.io.path.writeBytes
77+
import kotlinx.coroutines.Dispatchers
78+
import kotlinx.coroutines.coroutineScope
79+
import kotlinx.coroutines.withContext
7080

7181
open class RemoteTemplateProvider : TemplateProvider {
7282

@@ -144,23 +154,27 @@ open class RemoteTemplateProvider : TemplateProvider {
144154
return doLoadTemplates(context, repo, remoteRepo.innerPath)
145155
}
146156

147-
protected fun doLoadTemplates(
157+
protected suspend fun doLoadTemplates(
148158
context: WizardContext,
149159
repo: MinecraftSettings.TemplateRepo,
150160
rawInnerPath: String
151-
): List<LoadedTemplate> {
161+
): List<LoadedTemplate> = withContext(Dispatchers.IO) { // don't run on EDT
152162
val remoteRootPath = RemoteTemplateRepo.getDestinationZip(repo.name)
153163
if (!remoteRootPath.exists()) {
154-
return emptyList()
164+
return@withContext emptyList()
155165
}
156166

157167
val archiveRoot = remoteRootPath.absolutePathString() + JarFileSystem.JAR_SEPARATOR
158168

159169
val fs = JarFileSystem.getInstance()
170+
160171
val rootFile = fs.refreshAndFindFileByPath(archiveRoot)
161-
?: return emptyList()
172+
?: return@withContext emptyList()
162173
val modalityState = context.modalityState
163-
rootFile.refreshSync(modalityState)
174+
175+
blockingContext {
176+
rootFile.refreshSync(modalityState)
177+
}
164178

165179
val innerPath = replaceVariables(rawInnerPath)
166180
val repoRoot = if (innerPath.isNotBlank()) {
@@ -170,10 +184,10 @@ open class RemoteTemplateProvider : TemplateProvider {
170184
}
171185

172186
if (repoRoot == null) {
173-
return emptyList()
187+
return@withContext emptyList()
174188
}
175189

176-
return TemplateProvider.findTemplates(modalityState, repoRoot)
190+
return@withContext TemplateProvider.findTemplates(modalityState, repoRoot)
177191
}
178192

179193
private fun replaceVariables(originalRepoUrl: String): String =

src/main/kotlin/creator/custom/providers/TemplateProvider.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ import com.intellij.serviceContainer.BaseKeyedLazyInstance
4545
import com.intellij.util.KeyedLazyInstance
4646
import com.intellij.util.xmlb.annotations.Attribute
4747
import java.util.ResourceBundle
48+
import java.util.concurrent.TimeUnit
4849
import javax.swing.JComponent
50+
import org.jetbrains.concurrency.runAsync
4951

5052
/**
5153
* Extensions responsible for creating a [TemplateDescriptor] based on whatever data it is provided in its configuration
@@ -151,8 +153,9 @@ interface TemplateProvider {
151153
}
152154

153155
try {
154-
return file.refreshSync(modalityState)
155-
?.inputStream?.reader()?.use { TemplateResourceBundle(it, parent) }
156+
return runAsync {
157+
file.inputStream.reader().use { TemplateResourceBundle(it, parent) }
158+
}.blockingGet(20, TimeUnit.MILLISECONDS)
156159
} catch (t: Throwable) {
157160
if (t is ControlFlowException) {
158161
return parent
@@ -171,8 +174,12 @@ interface TemplateProvider {
171174
tooltip: String? = null,
172175
bundle: ResourceBundle? = null
173176
): VfsLoadedTemplate? {
174-
descriptorFile.refreshSync(modalityState)
175-
var descriptor = Gson().fromJson<TemplateDescriptor>(descriptorFile.readText())
177+
var descriptor = runCatching {
178+
runAsync {
179+
descriptorFile.refreshSync(modalityState)
180+
Gson().fromJson<TemplateDescriptor>(descriptorFile.readText())
181+
}.blockingGet(100, TimeUnit.MILLISECONDS)
182+
}.getOrNull() ?: return null
176183
if (descriptor.version != TemplateDescriptor.FORMAT_VERSION) {
177184
thisLogger().warn("Cannot handle template ${descriptorFile.path} of version ${descriptor.version}")
178185
return null

src/main/kotlin/update/PluginUpdater.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ package com.demonwav.mcdev.update
2323
import com.demonwav.mcdev.util.findDeclaredField
2424
import com.demonwav.mcdev.util.forEachNotNull
2525
import com.demonwav.mcdev.util.invokeLater
26-
import com.demonwav.mcdev.util.invokeLaterAny
2726
import com.intellij.ide.plugins.IdeaPluginDescriptor
2827
import com.intellij.ide.plugins.PluginManagerCore
2928
import com.intellij.ide.plugins.PluginManagerMain
3029
import com.intellij.ide.plugins.PluginNode
3130
import com.intellij.ide.plugins.RepositoryHelper
3231
import com.intellij.openapi.application.ApplicationInfo
3332
import com.intellij.openapi.application.ApplicationManager
33+
import com.intellij.openapi.application.ModalityState
3434
import com.intellij.openapi.progress.ProgressIndicator
3535
import com.intellij.openapi.progress.ProgressManager
3636
import com.intellij.openapi.progress.Task
@@ -58,7 +58,7 @@ object PluginUpdater {
5858
.forEachNotNull { updateStatus = updateStatus.mergeWith(checkUpdatesInCustomRepo(it)) }
5959

6060
val finalUpdate = updateStatus
61-
invokeLaterAny { callback(finalUpdate) }
61+
invokeLater(ModalityState.any()) { callback(finalUpdate) }
6262
} catch (e: Exception) {
6363
PluginUpdateStatus.CheckFailed("Minecraft Development plugin update check failed")
6464
}

src/main/kotlin/util/files.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,20 @@
2020

2121
package com.demonwav.mcdev.util
2222

23-
import com.intellij.openapi.application.ApplicationManager
2423
import com.intellij.openapi.application.ModalityState
24+
import com.intellij.openapi.application.TransactionGuard
2525
import com.intellij.openapi.vfs.LocalFileSystem
2626
import com.intellij.openapi.vfs.VfsUtilCore
2727
import com.intellij.openapi.vfs.VirtualFile
2828
import com.intellij.openapi.vfs.newvfs.RefreshQueue
29+
import com.intellij.util.application
2930
import java.io.File
3031
import java.io.IOException
3132
import java.nio.file.Path
3233
import java.util.jar.Attributes
3334
import java.util.jar.JarFile
3435
import java.util.jar.Manifest
36+
import javax.swing.SwingUtilities
3537

3638
val VirtualFile.localFile: File
3739
get() = VfsUtilCore.virtualToIoFile(this)
@@ -79,17 +81,9 @@ operator fun Manifest.get(attribute: String): String? = mainAttributes.getValue(
7981
operator fun Manifest.get(attribute: Attributes.Name): String? = mainAttributes.getValue(attribute)
8082

8183
fun VirtualFile.refreshSync(modalityState: ModalityState): VirtualFile? {
82-
fun refresh() {
84+
tryWriteSafeContext(modalityState) {
8385
RefreshQueue.getInstance().refresh(false, this.isDirectory, null, modalityState, this)
8486
}
8587

86-
if (ApplicationManager.getApplication().isWriteAccessAllowed) {
87-
refresh()
88-
} else {
89-
runWriteTask {
90-
refresh()
91-
}
92-
}
93-
9488
return this.parent?.findOrCreateChildData(this, this.name)
9589
}

0 commit comments

Comments
 (0)