diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorInlayLensManager.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorInlayLensManager.kt index 97f67c0..189847f 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorInlayLensManager.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorInlayLensManager.kt @@ -50,7 +50,7 @@ class EditorInlayLensManager private constructor(private val editor: Editor) { private val inlays = mutableMapOf<RangeHighlighter, Inlay<LensRenderer>>() - fun show(highlighterWithInfo: HighlighterWithInfo) { + private fun show(highlighterWithInfo: HighlighterWithInfo) { val (highlighter, info) = highlighterWithInfo val currentInlay = inlays[highlighter] if (currentInlay != null && currentInlay.isValid) { @@ -73,10 +73,14 @@ class EditorInlayLensManager private constructor(private val editor: Editor) { executeInInlayBatchMode(highlightersWithInfo.size) { highlightersWithInfo.forEach(::show) } } - fun hide(highlighter: RangeHighlighter) { + private fun hide(highlighter: RangeHighlighter) { inlays.remove(highlighter)?.dispose() } + fun hideAll(highlighters: Collection<RangeHighlighter>) { + executeInInlayBatchMode(highlighters.size) { highlighters.forEach(::hide) } + } + fun hideAll() { executeInInlayBatchMode(inlays.size) { inlays.values.forEach(Inlay<*>::dispose) } inlays.clear() diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensMarkupModelListener.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensMarkupModelListener.kt index 6fc76b6..603ea65 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensMarkupModelListener.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensMarkupModelListener.kt @@ -1,6 +1,7 @@ package com.chylex.intellij.inspectionlens.editor import com.chylex.intellij.inspectionlens.InspectionLensPluginDisposableService +import com.chylex.intellij.inspectionlens.utils.DebouncingInvokeOnDispatchThread import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.lang.annotation.HighlightSeverity import com.intellij.openapi.application.ApplicationManager @@ -21,6 +22,9 @@ import com.jetbrains.rd.util.lifetime.intersect class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelListener { private val lens = EditorInlayLensManager.getOrCreate(editor) + private val showOnDispatchThread = DebouncingInvokeOnDispatchThread(lens::showAll) + private val hideOnDispatchThread = DebouncingInvokeOnDispatchThread(lens::hideAll) + override fun afterAdded(highlighter: RangeHighlighterEx) { showIfValid(highlighter) } @@ -30,35 +34,19 @@ class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelL } override fun beforeRemoved(highlighter: RangeHighlighterEx) { - lens.hide(highlighter) + if (getFilteredHighlightInfo(highlighter) != null) { + hideOnDispatchThread.enqueue(highlighter) + } } private fun showIfValid(highlighter: RangeHighlighter) { - runWithHighlighterIfValid(highlighter, lens::show, ::showAsynchronously) - } - - private fun showAllValid(highlighters: Array<RangeHighlighter>) { - val immediateHighlighters = mutableListOf<HighlighterWithInfo>() - - for (highlighter in highlighters) { - runWithHighlighterIfValid(highlighter, immediateHighlighters::add, ::showAsynchronously) - } - - lens.showAll(immediateHighlighters) + runWithHighlighterIfValid(highlighter, showOnDispatchThread::enqueue, ::showAsynchronously) } private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) { highlighterWithInfo.requestDescription { if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) { - val application = ApplicationManager.getApplication() - if (application.isDispatchThread) { - lens.show(highlighterWithInfo) - } - else { - application.invokeLater { - lens.show(highlighterWithInfo) - } - } + showOnDispatchThread.enqueue(highlighterWithInfo) } } } @@ -66,15 +54,12 @@ class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelL companion object { private val MINIMUM_SEVERITY = HighlightSeverity.TEXT_ATTRIBUTES.myVal + 1 - private fun getHighlightInfoIfValid(highlighter: RangeHighlighter): HighlightInfo? { - return if (highlighter.isValid) - HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { it.severity.myVal >= MINIMUM_SEVERITY } - else - null + private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? { + return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { it.severity.myVal >= MINIMUM_SEVERITY } } private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) { - val info = getHighlightInfoIfValid(highlighter) + val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo) if (info != null) { processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync) } @@ -91,7 +76,7 @@ class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelL /** * Attaches a new [LensMarkupModelListener] to the document model of the provided [TextEditor], and reports all existing inspection highlights to [EditorInlayLensManager]. - * + * * The [LensMarkupModelListener] will be disposed when either the [TextEditor] is disposed, or via [InspectionLensPluginDisposableService] when the plugin is unloaded. */ fun install(textEditor: TextEditor) { @@ -103,7 +88,10 @@ class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelL val listener = LensMarkupModelListener(editor) markupModel.addMarkupModelListener(pluginLifetime.intersect(editorLifetime).createNestedDisposable(), listener) - listener.showAllValid(markupModel.allHighlighters) + + for (highlighter in markupModel.allHighlighters) { + listener.showIfValid(highlighter) + } } } } diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/utils/DebouncingInvokeOnDispatchThread.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/utils/DebouncingInvokeOnDispatchThread.kt new file mode 100644 index 0000000..cae2a45 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/utils/DebouncingInvokeOnDispatchThread.kt @@ -0,0 +1,32 @@ +package com.chylex.intellij.inspectionlens.utils + +import com.intellij.openapi.application.ApplicationManager + +class DebouncingInvokeOnDispatchThread<T>(private val action: (List<T>) -> Unit) { + private var queuedItems = mutableListOf<T>() + private var isEnqueued = false + + fun enqueue(item: T) { + synchronized(this) { + queuedItems.add(item) + + // Enqueue even if already on dispatch thread to debounce consecutive calls. + if (!isEnqueued) { + isEnqueued = true + ApplicationManager.getApplication().invokeLater(::process) + } + } + } + + private fun process() { + var itemsToProcess: List<T> + + synchronized(this) { + itemsToProcess = queuedItems + queuedItems = mutableListOf() + isEnqueued = false + } + + action(itemsToProcess) + } +}