[editor] IJPL-181752 Fix for permanent freeze in editor opening if an exception happens during composite creation

This commit improves exception handling to make sure that `waitForAvailable` eventually finishes. Otherwise, opened editor freezes forever


(cherry picked from commit b4fad212beab73307838e2360cc0db061a7aa8e7)

IJ-CR-159309

GitOrigin-RevId: 32fe240a77bdeff687ee5470f56e21d8b5d5fbd9
This commit is contained in:
Alexandr Trushev
2025-04-02 18:10:36 +02:00
committed by intellij-monorepo-bot
parent e17ab5b1b9
commit 7ec6f2a4a7

View File

@@ -31,6 +31,7 @@ import com.intellij.openapi.project.PossiblyDumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.Pair
import com.intellij.openapi.util.Weighted
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.FocusWatcher
@@ -57,6 +58,7 @@ import java.awt.event.FocusAdapter
import java.awt.event.FocusEvent
import java.util.concurrent.TimeUnit
import javax.swing.*
import kotlin.collections.firstOrNull
private val LOG = logger<EditorComposite>()
@@ -188,8 +190,7 @@ open class EditorComposite internal constructor(
if (fileEditorWithProviders.isEmpty()) {
withContext(Dispatchers.EDT) {
compositePanel.removeAll()
this@EditorComposite.fileEditorWithProviders.value = emptyList()
_selectedEditorWithProvider.value = null
setFileEditors(fileEditors = emptyList(), selectedEditor = null)
}
return
}
@@ -203,35 +204,11 @@ open class EditorComposite internal constructor(
}
val beforePublisher = project.messageBus.syncAndPreloadPublisher(FileEditorManagerListener.Before.FILE_EDITOR_MANAGER)
val selectedFileEditor = if (model.state == null) {
(serviceAsync<FileEditorProviderManager>() as FileEditorProviderManagerImpl).getSelectedFileEditorProvider(
file = file,
fileEditorWithProviders = fileEditorWithProviders,
editorHistoryManager = project.serviceAsync<EditorHistoryManager>(),
)
}
else {
model.state.selectedProvider?.let { selectedProvider ->
fileEditorWithProviders.firstOrNull { it.provider.editorTypeId == selectedProvider }?.provider
}
}
val selectedFileEditor = getSelectedEditor(fileEditorWithProviders, model.state)
// read not in EDT
val isNewEditor = true
val states = fileEditorWithProviders.map { (_, provider) ->
if (model.state == null) {
if (isNewEditor) {
// We have to try to get state from the history only in case of the editor is not opened.
// Otherwise, history entry might have a state out of sync with the current editor state.
project.serviceAsync<EditorHistoryManager>().getState(file, provider)
}
else {
null
}
}
else {
model.state.providers.get(provider.editorTypeId)?.let { provider.readState(it, project, file) }
}
getEditorState(provider, model.state)
}
val fileEditorManager = project.serviceAsync<FileEditorManager>()
@@ -240,7 +217,10 @@ open class EditorComposite internal constructor(
span("file opening in EDT and repaint", Dispatchers.EDT) {
span("beforeFileOpened event executing") {
blockingContext {
beforePublisher!!.beforeFileOpened(fileEditorManager, file)
computeOrLogException(
lambda = { beforePublisher!!.beforeFileOpened(fileEditorManager, file) },
errorMessage = { "exception during beforeFileOpened notification" },
)
}
}
@@ -356,12 +336,12 @@ open class EditorComposite internal constructor(
selectedFileEditorProvider: FileEditorProvider?,
) = WriteIntentReadAction.run {
for ((index, fileEditorWithProvider) in fileEditorWithProviders.withIndex()) {
restoreEditorState(
fileEditorWithProvider = fileEditorWithProvider,
state = states.get(index) ?: continue,
exactState = false,
project = project,
)
states.get(index)?.also { state ->
computeOrLogException(
lambda = { restoreEditorState(fileEditorWithProvider, state, exactState = false, project) },
errorMessage = { "failed to restore state for $fileEditorWithProvider" },
)
}
}
var fileEditorWithProviderToSelect = fileEditorWithProviders.firstOrNull()
@@ -380,15 +360,62 @@ open class EditorComposite internal constructor(
}
}
}
component.validate()
fileEditorWithProviderToSelect?.fileEditor?.selectNotify()
computeOrLogException(
lambda = {
// ensure FileEditor's component has valid boundaries after creation
// Otherwise, the listeners may get an invalid state for AsyncFileEditorProvider right after creation.
// Ex: OpenFileDescriptor is trying to scroll zero-height component in the 'FileEditorManager.runWhenLoaded' callback
component.validate()
},
errorMessage = { "failed to validate panel component" },
)
computeOrLogException(
lambda = { fileEditorWithProviderToSelect?.fileEditor?.selectNotify() },
errorMessage = { "exception during selectNotify" },
)
// Only after applyFileEditorsInEdt - for external clients composite API should use _actual_ _applied_ state, not intermediate.
// For example, see EditorHistoryManager -
// we will get assertion if we return a non-empty list of editors but do not set selected file editor.
this.fileEditorWithProviders.value = fileEditorWithProviders
_selectedEditorWithProvider.value = fileEditorWithProviderToSelect
setFileEditors(fileEditorWithProviders, fileEditorWithProviderToSelect)
}
private suspend fun getSelectedEditor(fileEditorWithProviders: List<FileEditorWithProvider>, state: FileEntry?): FileEditorProvider? {
return if (state != null) {
state.selectedProvider?.let { selectedProvider ->
fileEditorWithProviders.firstOrNull { it.provider.editorTypeId == selectedProvider }?.provider
}
}
else {
val providerManager = serviceAsync<FileEditorProviderManager>() as FileEditorProviderManagerImpl
val historyManager = project.serviceAsync<EditorHistoryManager>()
computeOrLogException(
lambda = { providerManager.getSelectedFileEditorProvider(file, fileEditorWithProviders, historyManager) },
errorMessage = { "failed to choose selected editor" },
)
}
}
private suspend fun getEditorState(provider: FileEditorProvider, state: FileEntry?): FileEditorState? {
return if (state != null) {
state.providers.get(provider.editorTypeId)?.let {
computeOrLogException(
lambda = { provider.readState(it, project, file) },
errorMessage = { "failed to read editor state" },
)
}
}
else {
// We have to try to get state from the history only in case of the editor is not opened.
// Otherwise, history entry might have a state out of sync with the current editor state.
val historyManager = project.serviceAsync<EditorHistoryManager>()
computeOrLogException(
lambda = { historyManager.getState(file, provider) },
errorMessage = { "failed to read editor state" },
)
}
}
private fun setTabbedPaneComponent(tabbedPaneWrapper: TabbedPaneWrapper) {
@@ -404,6 +431,11 @@ open class EditorComposite internal constructor(
)
}
private fun setFileEditors(fileEditors: List<FileEditorWithProvider>, selectedEditor: FileEditorWithProvider?) {
fileEditorWithProviders.value = fileEditors
_selectedEditorWithProvider.value = selectedEditor
}
@get:Deprecated("use {@link #getAllEditorsWithProviders()}", ReplaceWith("allProviders"), level = DeprecationLevel.ERROR)
val providers: Array<FileEditorProvider>
get() = providerSequence.toList().toTypedArray()
@@ -799,6 +831,20 @@ open class EditorComposite internal constructor(
return element
}
/**
* Never rethrows exceptions from [lambda] to make sure that [setFileEditors] is always invoked releasing [waitForAvailable].
*
* IJPL-181752 logging all exceptions including PCE may not be the best solution,
* but it can make life easier while investigating why the selected editor / editor state is null
*/
private fun <T> computeOrLogException(lambda: () -> T, errorMessage: () -> String): T? {
return runCatching {
lambda.invoke()
}.onFailure {
LOG.error(errorMessage.invoke(), it)
}.getOrNull()
}
override fun toString() = "EditorComposite(identityHashCode=${System.identityHashCode(this)}, file=$file)"
}
@@ -937,9 +983,9 @@ internal fun isEditorComposite(component: Component): Boolean = component is Edi
* A mapper for old API with arrays and pairs
*/
@Internal
fun retrofitEditorComposite(composite: FileEditorComposite?): com.intellij.openapi.util.Pair<Array<FileEditor>, Array<FileEditorProvider>> {
fun retrofitEditorComposite(composite: FileEditorComposite?): Pair<Array<FileEditor>, Array<FileEditorProvider>> {
if (composite == null) {
return com.intellij.openapi.util.Pair(FileEditor.EMPTY_ARRAY, FileEditorProvider.EMPTY_ARRAY)
return Pair(FileEditor.EMPTY_ARRAY, FileEditorProvider.EMPTY_ARRAY)
}
else {
return composite.retrofit()