diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManager.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManager.kt index 4b32943..822f3ef 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManager.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManager.kt @@ -4,7 +4,7 @@ import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.InlayProperties -import com.intellij.openapi.editor.ex.RangeHighlighterEx +import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.util.Key /** @@ -18,6 +18,14 @@ class EditorInlayLensManager private constructor(private val editor: Editor) { return editor.getUserData(KEY) ?: EditorInlayLensManager(editor).also { editor.putUserData(KEY, it) } } + fun remove(editor: Editor) { + val manager = editor.getUserData(KEY) + if (manager != null) { + manager.hideAll() + editor.putUserData(KEY, null) + } + } + private fun updateRenderer(renderer: LensRenderer, info: HighlightInfo) { renderer.text = info.description.takeIf(String::isNotBlank)?.let(::addMissingPeriod) ?: " " renderer.severity = LensSeverity.from(info.severity) @@ -28,9 +36,9 @@ class EditorInlayLensManager private constructor(private val editor: Editor) { } } - private val inlays = mutableMapOf<RangeHighlighterEx, Inlay<LensRenderer>>() + private val inlays = mutableMapOf<RangeHighlighter, Inlay<LensRenderer>>() - fun show(highlighter: RangeHighlighterEx, info: HighlightInfo) { + fun show(highlighter: RangeHighlighter, info: HighlightInfo) { val currentInlay = inlays[highlighter] if (currentInlay != null && currentInlay.isValid) { updateRenderer(currentInlay.renderer, info) @@ -47,7 +55,12 @@ class EditorInlayLensManager private constructor(private val editor: Editor) { } } - fun hide(highlighter: RangeHighlighterEx) { + fun hide(highlighter: RangeHighlighter) { inlays.remove(highlighter)?.dispose() } + + fun hideAll() { + inlays.values.forEach(Inlay<*>::dispose) + inlays.clear() + } } diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/InspectionLensPluginDisposableService.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/InspectionLensPluginDisposableService.kt new file mode 100644 index 0000000..59ce4a1 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/InspectionLensPluginDisposableService.kt @@ -0,0 +1,10 @@ +package com.chylex.intellij.inspectionlens + +import com.intellij.openapi.Disposable + +/** + * Gets automatically disposed when the plugin is unloaded. + */ +class InspectionLensPluginDisposableService : Disposable { + override fun dispose() {} +} diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/InspectionLensPluginListener.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/InspectionLensPluginListener.kt new file mode 100644 index 0000000..af0df07 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/InspectionLensPluginListener.kt @@ -0,0 +1,43 @@ +package com.chylex.intellij.inspectionlens + +import com.intellij.ide.plugins.DynamicPluginListener +import com.intellij.ide.plugins.IdeaPluginDescriptor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.project.ProjectManager + +/** + * Handles dynamic plugin loading. + * + * On load, it installs the [LensMarkupModelListener] to all open editors. + * On unload, it removes all lenses from all open editors. + */ +class InspectionLensPluginListener : DynamicPluginListener { + companion object { + private const val PLUGIN_ID = "com.chylex.intellij.inspectionlens" + + private inline fun ProjectManager.forEachEditor(action: (TextEditor) -> Unit) { + for (project in this.openProjects.filterNot { it.isDisposed }) { + val fileEditorManager = FileEditorManager.getInstance(project) + + for (editor in fileEditorManager.allEditors.filterIsInstance<TextEditor>()) { + action(editor) + } + } + } + } + + override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) { + if (pluginDescriptor.pluginId.idString == PLUGIN_ID) { + ProjectManager.getInstanceIfCreated()?.forEachEditor(LensMarkupModelListener.Companion::install) + } + } + + override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) { + if (pluginDescriptor.pluginId.idString == PLUGIN_ID) { + ProjectManager.getInstanceIfCreated()?.forEachEditor { + EditorInlayLensManager.remove(it.editor) + } + } + } +} diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/LensFileEditorListener.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/LensFileEditorListener.kt index b551d89..402f96a 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/LensFileEditorListener.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/LensFileEditorListener.kt @@ -1,7 +1,5 @@ package com.chylex.intellij.inspectionlens -import com.intellij.openapi.editor.ex.MarkupModelEx -import com.intellij.openapi.editor.impl.DocumentMarkupModel import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.TextEditor @@ -9,18 +7,14 @@ import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider import com.intellij.openapi.vfs.VirtualFile /** - * Listens for newly opened editors, and attaches a [LensMarkupModelListener] to their document model. + * Listens for newly opened editors, and installs a [LensMarkupModelListener] on them. */ class LensFileEditorListener : FileEditorManagerListener { override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: MutableList<FileEditorWithProvider>) { for (editorWrapper in editorsWithProviders) { val fileEditor = editorWrapper.fileEditor if (fileEditor is TextEditor) { - val editor = fileEditor.editor - val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) - if (markupModel is MarkupModelEx) { - markupModel.addMarkupModelListener(fileEditor, LensMarkupModelListener(editor)) - } + LensMarkupModelListener.install(fileEditor) } } } diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/LensMarkupModelListener.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/LensMarkupModelListener.kt index 984e423..ddd43b1 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/LensMarkupModelListener.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/LensMarkupModelListener.kt @@ -1,16 +1,22 @@ package com.chylex.intellij.inspectionlens +import com.chylex.intellij.inspectionlens.util.MultiParentDisposable import com.intellij.codeInsight.daemon.impl.AsyncDescriptionSupplier import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.ex.MarkupModelEx import com.intellij.openapi.editor.ex.RangeHighlighterEx +import com.intellij.openapi.editor.impl.DocumentMarkupModel import com.intellij.openapi.editor.impl.event.MarkupModelListener +import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.fileEditor.TextEditor /** * Listens for inspection highlights and reports them to [EditorInlayLensManager]. */ -class LensMarkupModelListener(editor: Editor) : MarkupModelListener { +class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelListener { private val lens = EditorInlayLensManager.getOrCreate(editor) override fun afterAdded(highlighter: RangeHighlighterEx) { @@ -25,7 +31,7 @@ class LensMarkupModelListener(editor: Editor) : MarkupModelListener { lens.hide(highlighter) } - private fun showIfValid(highlighter: RangeHighlighterEx) { + private fun showIfValid(highlighter: RangeHighlighter) { if (!highlighter.isValid) { return } @@ -47,9 +53,35 @@ class LensMarkupModelListener(editor: Editor) : MarkupModelListener { } } - private fun showIfNonNullDescription(highlighter: RangeHighlighterEx, info: HighlightInfo) { + private fun showIfNonNullDescription(highlighter: RangeHighlighter, info: HighlightInfo) { if (info.description != null) { lens.show(highlighter, info) } } + + companion object { + /** + * 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) { + val editor = textEditor.editor + val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) + if (markupModel is MarkupModelEx) { + val pluginDisposable = ApplicationManager.getApplication().getService(InspectionLensPluginDisposableService::class.java) + + val listenerDisposable = MultiParentDisposable() + listenerDisposable.registerWithParent(textEditor) + listenerDisposable.registerWithParent(pluginDisposable) + + val listener = LensMarkupModelListener(editor) + markupModel.addMarkupModelListener(listenerDisposable.self, listener) + + for (highlighter in markupModel.allHighlighters) { + listener.showIfValid(highlighter) + } + } + } + } } diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/util/MultiParentDisposable.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/util/MultiParentDisposable.kt new file mode 100644 index 0000000..f8837d5 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/util/MultiParentDisposable.kt @@ -0,0 +1,18 @@ +package com.chylex.intellij.inspectionlens.util + +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import java.lang.ref.WeakReference + +/** + * A [Disposable] that can have multiple parents, and will be disposed when any parent is disposed. + * A [WeakReference] and a lambda will remain in memory for every parent that is not disposed. + */ +class MultiParentDisposable { + val self = Disposer.newDisposable() + + fun registerWithParent(parent: Disposable) { + val weakSelfReference = WeakReference(self) + Disposer.register(parent) { weakSelfReference.get()?.let(Disposer::dispose) } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 838c1a2..b2d07b8 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -11,6 +11,14 @@ <depends>com.intellij.modules.platform</depends> + <extensions defaultExtensionNs="com.intellij"> + <applicationService serviceImplementation="com.chylex.intellij.inspectionlens.InspectionLensPluginDisposableService" /> + </extensions> + + <applicationListeners> + <listener class="com.chylex.intellij.inspectionlens.InspectionLensPluginListener" topic="com.intellij.ide.plugins.DynamicPluginListener" /> + </applicationListeners> + <projectListeners> <listener class="com.chylex.intellij.inspectionlens.LensFileEditorListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" /> </projectListeners>