diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorComposite.kt b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorComposite.kt index ffca9a98742a..3db4516a4848 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorComposite.kt +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorComposite.kt @@ -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() @@ -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() as FileEditorProviderManagerImpl).getSelectedFileEditorProvider( - file = file, - fileEditorWithProviders = fileEditorWithProviders, - editorHistoryManager = project.serviceAsync(), - ) - } - 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().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() @@ -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, state: FileEntry?): FileEditorProvider? { + return if (state != null) { + state.selectedProvider?.let { selectedProvider -> + fileEditorWithProviders.firstOrNull { it.provider.editorTypeId == selectedProvider }?.provider + } + } + else { + val providerManager = serviceAsync() as FileEditorProviderManagerImpl + val historyManager = project.serviceAsync() + 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() + 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, selectedEditor: FileEditorWithProvider?) { + fileEditorWithProviders.value = fileEditors + _selectedEditorWithProvider.value = selectedEditor + } + @get:Deprecated("use {@link #getAllEditorsWithProviders()}", ReplaceWith("allProviders"), level = DeprecationLevel.ERROR) val providers: Array 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 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> { +fun retrofitEditorComposite(composite: FileEditorComposite?): Pair, Array> { 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()