show editor as early as possible - move building buildInitialFoldings to a separate task

GitOrigin-RevId: 50621466c7363d933ff565970949bb07907b2d37
This commit is contained in:
Vladimir Krivosheev
2023-07-07 02:59:35 +00:00
committed by intellij-monorepo-bot
parent ed00a296d5
commit edf0b232ec
5 changed files with 102 additions and 72 deletions
@@ -19,6 +19,7 @@ import com.intellij.openapi.actionSystem.CompositeDataProvider
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.readAction
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.components.serviceIfCreated
import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.logger
@@ -26,12 +27,10 @@ import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader.Companion.isEditorLoaded
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.blockingContextToIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Segment
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import java.util.concurrent.CancellationException
@@ -58,10 +57,8 @@ open class PsiAwareTextEditorImpl : TextEditorImpl {
override suspend fun loadEditorInBackground(): Runnable {
val editor = editor
val document = editor.document
class State {
var foldingState: CodeFoldingState? = null
var focusZones: List<Segment>? = null
var items: DocRenderPassFactory.Items? = null
var buffer: HintsBuffer? = null
@@ -70,16 +67,8 @@ open class PsiAwareTextEditorImpl : TextEditorImpl {
}
val state = State()
val psiManager = PsiManager.getInstance(project)
val psiManager = project.serviceAsync<PsiManager>()
readAction {
if (!project.isDefault && PsiDocumentManager.getInstance(project).isCommitted(document)) {
state.foldingState = catchingExceptions {
blockingContextToIndicator {
CodeFoldingManager.getInstance(project).buildInitialFoldings(document)
}
}
}
val psiFile = psiManager.findFile(file)
state.psiFile = psiFile
state.focusZones = catchingExceptions { FocusModePassFactory.calcFocusZones(psiFile) }
@@ -96,7 +85,6 @@ open class PsiAwareTextEditorImpl : TextEditorImpl {
}
return Runnable {
state.foldingState?.setToEditor(editor)
state.focusZones?.let { focusZones ->
FocusModePassFactory.setToEditor(focusZones, editor)
if (editor is EditorImpl) {
@@ -177,7 +165,7 @@ private inline fun <T : Any> catchingExceptionsAsync(computable: () -> T?): T? {
}
// not `inline` to ensure that this function is not used for a `suspend` task
private fun <T : Any> catchingExceptions(computable: () -> T?): T? {
internal fun <T : Any> catchingExceptions(computable: () -> T?): T? {
try {
return computable()
}
@@ -4,19 +4,27 @@ package com.intellij.openapi.fileEditor.impl.text
import com.intellij.codeHighlighting.BackgroundEditorHighlighter
import com.intellij.codeInsight.daemon.impl.TextEditorBackgroundHighlighter
import com.intellij.codeInsight.folding.CodeFoldingManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.EditorFactoryImpl
import com.intellij.openapi.fileEditor.*
import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader.Companion.isEditorLoaded
import com.intellij.openapi.fileEditor.impl.text.TextEditorImpl.Companion.createAsyncEditorLoader
import com.intellij.openapi.progress.blockingContextToIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.JDOMUtil
import com.intellij.openapi.util.WriteExternalException
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import org.jdom.Element
import org.jetbrains.annotations.NonNls
import java.util.function.Supplier
@@ -38,11 +46,39 @@ open class PsiAwareTextEditorProvider : TextEditorProvider(), AsyncFileEditorPro
val factory = EditorFactory.getInstance() as EditorFactoryImpl
val highlighter = asyncLoader.createHighlighterAsync(document, file)
val foldingState = if (project.isDefault) null else asyncLoader.coroutineScope.async(CoroutineName("buildInitialFoldings")) {
val codeFoldingManager = project.serviceAsync<CodeFoldingManager>()
readAction {
if (PsiDocumentManager.getInstance(project).isCommitted(document)) {
catchingExceptions {
blockingContextToIndicator {
codeFoldingManager.buildInitialFoldings(document)
}
}
}
else {
null
}
}
}
val tasks: List<suspend (EditorEx) -> Unit> = listOf(
{ editor -> configureHighlighter(highlighter, editor) },
{ editor ->
val state = foldingState?.await()
if (state != null) {
withContext(Dispatchers.EDT) {
state.setToEditor(editor)
}
}
}
)
return object : AsyncFileEditorProvider.Builder() {
override fun build(): FileEditor {
val editor = factory.createMainEditor(document, project, file)
val textEditor = PsiAwareTextEditorImpl(project = project, file = file, editor = editor, asyncLoader = asyncLoader)
asyncLoader.start(textEditor = textEditor, highlighterDeferred = highlighter)
asyncLoader.start(textEditor = textEditor, tasks = tasks)
return textEditor
}
}
@@ -36,7 +36,7 @@ private val ASYNC_LOADER = Key.create<AsyncEditorLoader>("ASYNC_LOADER")
class AsyncEditorLoader internal constructor(private val project: Project,
private val provider: TextEditorProvider,
private val coroutineScope: CoroutineScope) {
@JvmField val coroutineScope: CoroutineScope) {
private val delayedActions = ArrayDeque<Runnable>()
private var delayedScrollState: DelayedScrollState? = null
@@ -79,50 +79,75 @@ class AsyncEditorLoader internal constructor(private val project: Project,
// executed in the same EDT task where TextEditorImpl is created
@Internal
@RequiresEdt
fun start(textEditor: TextEditorImpl, highlighterDeferred: Deferred<EditorHighlighter>) {
fun start(textEditor: TextEditorImpl, tasks: List<suspend (EditorEx) -> Unit>) {
val editor = textEditor.editor
editor.putUserData(ASYNC_LOADER, this)
if (ApplicationManager.getApplication().isUnitTestMode) {
startInTests(highlighterDeferred = highlighterDeferred, editor = editor, textEditor = textEditor)
startInTests(tasks = tasks, editor = editor, textEditor = textEditor)
return
}
val continuationDeferred = coroutineScope.async {
textEditor.loadEditorInBackground()
}
// `openEditorImpl` uses runWithModalProgressBlocking,
// but an async editor load is performed in the background, out of the `openEditorImpl` call
val modality = ModalityState.any().asContextElement()
val taskJob = coroutineScope.async(modality) {
for (task in tasks) {
async { task(editor) }
}
}
// don't show yet another loading indicator on project open - use 3-second delay
val loadingDecorator = AsyncLoadingDecorator(
startDelay = if (EditorsSplitters.isOpenedInBulk(textEditor.file)) 3_000.milliseconds else 300.milliseconds,
)
// `openEditorImpl` uses runWithModalProgressBlocking,
// but an async editor load is performed in the background, out of the `openEditorImpl` call
val modality = ModalityState.any().asContextElement()
val editorComponent = textEditor.component
val indicatorJob = loadingDecorator.startLoading(scope = coroutineScope + modality, addUi = editorComponent::addLoadingDecoratorUi)
val configureHighlighterJob = coroutineScope.launch(modality) {
configureHighlighter(highlighterDeferred, editor)
}
val continuationDeferred = coroutineScope.async {
textEditor.loadEditorInBackground()
}
coroutineScope.launch(modality) {
val continuation = continuationDeferred.await()
configureHighlighterJob.join()
loadingDecorator.stopLoading(scope = this, indicatorJob = indicatorJob)
// await instead of joint to get errors here
taskJob.await()
indicatorJob.cancel()
withContext(Dispatchers.EDT) {
loaded(continuation = continuation, editor = editor)
editor.putUserData(ASYNC_LOADER, null)
editor.scrollingModel.disableAnimation()
try {
delayedScrollState?.let {
delayedScrollState = null
restoreCaretPosition(editor = editor, delayedScrollState = it, coroutineScope = coroutineScope)
}
loadingDecorator.stopLoading(scope = this)
runCatching {
continuation?.run()
}.getOrLogException(logger<AsyncEditorLoader>())
while (true) {
(delayedActions.pollFirst() ?: break).run()
}
}
finally {
editor.scrollingModel.enableAnimation()
}
}
EditorNotifications.getInstance(project).updateNotifications(textEditor.file)
}
}
private fun startInTests(highlighterDeferred: Deferred<EditorHighlighter>, editor: EditorEx, textEditor: TextEditorImpl) {
private fun startInTests(tasks: List<suspend (EditorEx) -> Unit>, editor: EditorEx, textEditor: TextEditorImpl) {
val continuation = runWithModalProgressBlocking(project, "") {
withRawProgressReporter { // required for switch to
configureHighlighter(highlighterDeferred, editor)
// required for switch to
withRawProgressReporter {
tasks.map { it(editor) }
textEditor.loadEditorInBackground()
}
}
@@ -139,30 +164,6 @@ class AsyncEditorLoader internal constructor(private val project: Project,
}
}
private fun loaded(continuation: Runnable?, editor: EditorEx) {
editor.putUserData(ASYNC_LOADER, null)
runCatching {
continuation?.run()
}.getOrLogException(logger<AsyncEditorLoader>())
editor.scrollingModel.disableAnimation()
delayedScrollState?.let {
delayedScrollState = null
restoreCaretPosition(editor = editor, delayedScrollState = it, coroutineScope = coroutineScope)
}
try {
while (true) {
(delayedActions.pollFirst() ?: break).run()
}
}
finally {
editor.scrollingModel.enableAnimation()
}
}
@RequiresReadLock
fun getEditorState(level: FileEditorStateLevel, editor: Editor): TextEditorState {
return provider.getStateImpl(project, editor, level)
@@ -194,18 +195,18 @@ private fun restoreCaretPosition(editor: EditorEx, delayedScrollState: DelayedSc
val viewport = editor.scrollPane.viewport
fun isReady(): Boolean {
val extentSize = viewport.extentSize
val extentSize = viewport.extentSize?.takeIf { it.width != 0 && it.height != 0 } ?: viewport.preferredSize
return extentSize.width != 0 && extentSize.height != 0
}
if (isReady()) {
if (viewport.isShowing && isReady()) {
doScroll()
}
else {
var listenerHandle: DisposableHandle? = null
val listener = object : javax.swing.event.ChangeListener {
override fun stateChanged(e: ChangeEvent) {
if (!isReady()) {
if (!viewport.isShowing || !isReady()) {
return
}
@@ -222,7 +223,7 @@ private fun restoreCaretPosition(editor: EditorEx, delayedScrollState: DelayedSc
}
}
private suspend fun configureHighlighter(highlighterDeferred: Deferred<EditorHighlighter>, editor: EditorEx) {
suspend fun configureHighlighter(highlighterDeferred: Deferred<EditorHighlighter>, editor: EditorEx) {
val highlighter = highlighterDeferred.await()
subtask("editor highlighter set", Dispatchers.EDT) {
editor.settings.setLanguageSupplier { getDocumentLanguage(editor) }
@@ -1,6 +1,8 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.fileEditor.impl.text
import com.intellij.diagnostic.StartUpMeasurer
import com.intellij.diagnostic.rootTask
import com.intellij.ide.IdeBundle
import com.intellij.ide.structureView.StructureViewBuilder
import com.intellij.lang.Language
@@ -28,6 +30,7 @@ import org.jetbrains.annotations.NonNls
import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
import javax.swing.JComponent
import kotlin.coroutines.EmptyCoroutineContext
private val TRANSIENT_EDITOR_STATE_KEY = Key.create<TransientEditorState>("transientState")
@@ -46,8 +49,9 @@ open class TextEditorImpl @Internal constructor(@JvmField protected val project:
file = file,
editor = editor,
asyncLoader = createAsyncEditorLoader(provider, project)) {
val highlighter = asyncLoader.createHighlighterAsync(editor.document, file)
@Suppress("LeakingThis")
asyncLoader.start(textEditor = this, highlighterDeferred = asyncLoader.createHighlighterAsync(editor.document, file))
asyncLoader.start(textEditor = this, tasks = listOf { configureHighlighter(highlighter, it) })
}
init {
@@ -70,9 +74,12 @@ open class TextEditorImpl @Internal constructor(@JvmField protected val project:
// don't pollute global scope
companion object {
fun createAsyncEditorLoader(provider: TextEditorProvider, project: Project): AsyncEditorLoader {
return AsyncEditorLoader(project = project,
provider = provider,
coroutineScope = project.service<AsyncEditorLoaderService>().coroutineScope.childScope(supervisor = false))
val context = if (StartUpMeasurer.isEnabled()) rootTask() else EmptyCoroutineContext
return AsyncEditorLoader(
project = project,
provider = provider,
coroutineScope = project.service<AsyncEditorLoaderService>().coroutineScope.childScope(supervisor = false, context = context),
)
}
@Internal
@@ -37,9 +37,7 @@ internal class AsyncLoadingDecorator(private val startDelay: Duration) {
}
}
fun stopLoading(scope: CoroutineScope, indicatorJob: Job) {
// no need to join - executed in EDT
indicatorJob.cancel()
fun stopLoading(scope: CoroutineScope) {
scope.launch(Dispatchers.EDT) {
val loadingLayer = loadingLayer ?: return@launch
try {