1
0
mirror of https://github.com/chylex/IntelliJ-Inspection-Lens.git synced 2025-09-17 23:24:49 +02:00

8 Commits

9 changed files with 175 additions and 47 deletions

View File

@@ -6,7 +6,7 @@ plugins {
}
group = "com.chylex.intellij.inspectionlens"
version = "1.5.1"
version = "1.5.2"
repositories {
mavenCentral()
@@ -18,11 +18,8 @@ repositories {
dependencies {
intellijPlatform {
intellijIdeaUltimate("2023.3.3")
intellijIdeaUltimate("2024.2")
bundledPlugin("tanvd.grazi")
// https://plugins.jetbrains.com/plugin/12175-grazie-lite/versions
// plugin("tanvd.grazi", "233.13135.14")
}
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
@@ -31,10 +28,19 @@ dependencies {
intellijPlatform {
pluginConfiguration {
ideaVersion {
sinceBuild.set("233.11361.10")
sinceBuild.set("242")
untilBuild.set(provider { null })
}
}
pluginVerification {
freeArgs.add("-mute")
freeArgs.add("TemplateWordInPluginId")
ides {
recommended()
}
}
}
kotlin {
@@ -42,16 +48,12 @@ kotlin {
compilerOptions {
freeCompilerArgs = listOf(
"-X" + "jvm-default=all"
"-X" + "jvm-default=all",
"-X" + "lambdas=indy"
)
}
}
tasks.withType<Test>().configureEach {
tasks.test {
useJUnitPlatform()
}
val testSnapshot by intellijPlatformTesting.testIde.registering {
version = "LATEST-EAP-SNAPSHOT"
useInstaller = false
}

View File

@@ -2,7 +2,7 @@ rootProject.name = "InspectionLens"
pluginManagement {
plugins {
kotlin("jvm") version "1.9.21"
id("org.jetbrains.intellij.platform") version "2.2.1"
kotlin("jvm") version "1.9.24" // https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#bundled-stdlib-versions
id("org.jetbrains.intellij.platform") version "2.6.0" // https://github.com/JetBrains/intellij-platform-gradle-plugin/releases
}
}

View File

@@ -3,6 +3,7 @@ package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.ProjectManager
@@ -13,6 +14,8 @@ import com.intellij.openapi.project.ProjectManager
internal object InspectionLens {
const val PLUGIN_ID = "com.chylex.intellij.inspectionlens"
val LOG = logger<InspectionLens>()
/**
* Installs lenses into [editor].
*/

View File

@@ -2,6 +2,7 @@ package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.event.MarkupModelListener
@@ -53,7 +54,7 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
if (info != null) {
if (info != null && !UpdateHighlightersUtil.isFileLevelOrGutterAnnotation(info)) {
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
}
}

View File

@@ -1,54 +1,132 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.InspectionLens
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescriptor
import com.intellij.codeInsight.daemon.impl.IntentionsUI
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction
import com.intellij.codeInsight.intention.impl.CachedIntentions
import com.intellij.codeInsight.intention.impl.IntentionHintComponent
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.lang.LangBundle
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase
import com.intellij.ui.awt.RelativePoint
import com.intellij.util.concurrency.AppExecutorUtil
import java.lang.reflect.Method
internal object IntentionsPopup {
fun show(editor: Editor) {
if (!tryShow(editor)) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
private const val INTENTION_SOURCE_CLASS_NAME = "com.intellij.codeInsight.intention.IntentionSource"
private val showPopupMethod: ShowPopupMethod? = try {
val method = IntentionHintComponent::class.java.declaredMethods.first { method ->
val parameterTypes = method.parameterTypes
method.name == "showPopup" &&
parameterTypes.size in 1..2 &&
parameterTypes[0] === RelativePoint::class.java &&
parameterTypes.getOrNull(1).let { p -> p == null || p.name == INTENTION_SOURCE_CLASS_NAME }
}
method.isAccessible = true
@Suppress("UNCHECKED_CAST")
val args: Array<Any?> = if (method.parameterCount == 1)
arrayOf(null)
else
arrayOf(null, (Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>>).enumConstants.first { it.name == "OTHER" })
ShowPopupMethod(method, args)
} catch (t: Throwable) {
InspectionLens.LOG.warn("Could not initialize intention popup", t)
null
}
private class ShowPopupMethod(private val method: Method, private val args: Array<Any?>) {
operator fun invoke(component: IntentionHintComponent) {
method.invoke(component, *args)
}
}
private fun tryShow(editor: Editor): Boolean {
// If the IDE uses the default Show Intentions action and handler,
// use the handler directly to bypass additional logic from the action.
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
if (action.javaClass === ShowIntentionActionsAction::class.java) {
return tryShowWithDefaultHandler(editor)
}
else {
ActionUtil.invokeAction(action, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
return true
fun show(highlightInfo: HighlightInfo, inlay: Inlay<*>) {
if (!tryShow(highlightInfo, inlay)) {
showNoActionsAvailable(inlay.editor)
}
}
private fun tryShowWithDefaultHandler(editor: Editor): Boolean {
private fun tryShow(highlightInfo: HighlightInfo, inlay: Inlay<*>): Boolean {
val editor = inlay.editor
val project = editor.project ?: return false
val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false
PsiDocumentManager.getInstance(project).commitAllDocuments()
IntentionsUI.getInstance(project).hide()
HANDLER.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu = true)
ReadAction
.nonBlocking<IntentionsInfo> { collectIntentions(editor, project, file, highlightInfo, inlay.offset) }
.finishOnUiThread(ModalityState.current()) { tryShowPopup(project, file, editor, it) }
.submit(AppExecutorUtil.getAppExecutorService())
return true
}
private val HANDLER = object : ShowIntentionActionsHandler() {
public override fun showIntentionHint(project: Project, editor: Editor, file: PsiFile, showFeedbackOnEmptyMenu: Boolean) {
super.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu)
private fun collectIntentions(editor: Editor, project: Project, file: PsiFile, info: HighlightInfo, offset: Int): IntentionsInfo {
val intentions = mutableListOf<IntentionActionDescriptor>()
info.findRegisteredQuickFix { descriptor, _ ->
if (DumbService.getInstance(project).isUsableInCurrentContext(descriptor.action) && ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
intentions.add(descriptor)
}
null
}
return IntentionsInfo().also {
it.offset = offset
if (info.severity === HighlightSeverity.ERROR) {
it.errorFixesToShow.addAll(intentions)
}
else {
it.inspectionFixesToShow.addAll(intentions)
}
}
}
private fun tryShowPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
try {
showPopup(project, file, editor, intentions)
} catch (t: Throwable) {
InspectionLens.LOG.error("Could not show intention popup", t)
showNoActionsAvailable(editor)
}
}
private fun showPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
if (intentions.isEmpty || showPopupMethod == null) {
val showIntentionsAction = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
ActionUtil.invokeAction(showIntentionsAction, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
}
else {
val cachedIntentions = CachedIntentions.create(project, file, editor, intentions)
val hintComponent = IntentionHintComponent.showIntentionHint(project, file, editor, false, cachedIntentions)
showPopupMethod.invoke(hintComponent)
}
}
private fun showNoActionsAvailable(editor: Editor) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
}
}

View File

@@ -44,7 +44,7 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
fun setPropertiesFrom(info: HighlightInfo) {
this.info = info
val description = getValidDescriptionText(info.description)
val description = getValidDescriptionText(info.description, settings.maxDescriptionLength)
text = description
attributes = LensSeverity.from(info.severity).textAttributes
@@ -125,7 +125,7 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
moveToOffset(editor, info.actualStartOffset)
if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
IntentionsPopup.show(editor)
IntentionsPopup.show(info, inlay)
}
}
}
@@ -155,15 +155,13 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
private const val HOVER_HORIZONTAL_PADDING = TEXT_HORIZONTAL_PADDING - 2
private const val UNDERLINE_WIDTH_REDUCTION = (TEXT_HORIZONTAL_PADDING * 2) - 1
private const val MAX_DESCRIPTION_LENGTH = 120
/**
* Kotlin compiler inspections have an `[UPPERCASE_TAG]` at the beginning.
*/
private val UPPERCASE_TAG_REGEX = Pattern.compile("^\\[[A-Z_]+] ")
private fun getValidDescriptionText(text: String?): String {
return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)))
private fun getValidDescriptionText(text: String?, maxLength: Int): String {
return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)), maxLength)
}
private fun stripUppercaseTag(text: String): String {
@@ -181,9 +179,9 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text
}
private fun addEllipsisOrMissingPeriod(text: String): String {
private fun addEllipsisOrMissingPeriod(text: String, maxLength: Int): String {
return when {
text.length > MAX_DESCRIPTION_LENGTH -> text.take(MAX_DESCRIPTION_LENGTH).trimEnd { it.isWhitespace() || it == '.' } + ""
text.length > maxLength -> text.take(maxLength).trimEnd { it.isWhitespace() || it == '.' } + ""
!text.endsWith('.') -> "$text."
else -> text
}

View File

@@ -10,6 +10,7 @@ import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.ConfigurableWithId
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.util.Disposer
import com.intellij.ui.DisabledTraversalPolicy
import com.intellij.ui.EditorTextFieldCellRenderer.SimpleRendererComponent
@@ -19,6 +20,7 @@ import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.RowLayout
import com.intellij.ui.dsl.builder.bindIntText
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel
@@ -75,16 +77,21 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
override fun createPanel(): DialogPanel {
val settings = settingsService.state
return panel {
lateinit var panel: DialogPanel
panel = panel {
group("Appearance") {
row {
checkBox("Use editor font").bindSelected(settings::useEditorFont)
}
row("Max description length:") {
intTextField(LensSettingsState.MAX_DESCRIPTION_LENGTH_RANGE, keyboardStep = 10).bindIntText(settings::maxDescriptionLength)
}
}
group("Behavior") {
row("Hover mode:") {
val items = LensHoverMode.values().toList()
val items = LensHoverMode.entries
val renderer = SimpleListCellRenderer.create("", LensHoverMode::description)
comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT }
}
@@ -105,8 +112,21 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
checkBox("Other").bindSelected(settings::showUnknownSeverities)
}
}
group("Actions") {
row {
button("Reset to Default") {
if (MessageDialogBuilder.yesNo("Reset to Default", "Are you sure you want to reset settings to default?").ask(panel)) {
settingsService.resetState()
reset()
}
}
}
}
}
return panel
}
private fun <K, V> Cell<JBCheckBox>.bindSelectedToNotIn(collection: MutableMap<K, V>, key: K, value: V): Cell<JBCheckBox> {
return bindSelected({ key !in collection }, { if (it) collection.remove(key) else collection[key] = value })

View File

@@ -21,9 +21,14 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
var showUnknownSeverities by property(true)
var useEditorFont by property(true)
var maxDescriptionLength by property(120)
var lensHoverMode by enum(LensHoverMode.DEFAULT)
}
companion object {
val MAX_DESCRIPTION_LENGTH_RANGE = 20..1000
}
@get:Synchronized
@set:Synchronized
var severityFilter = createSeverityFilter()
@@ -32,11 +37,27 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
val useEditorFont
get() = state.useEditorFont
val maxDescriptionLength
get() = state.maxDescriptionLength
val lensHoverMode
get() = state.lensHoverMode
override fun loadState(state: State) {
super.loadState(state)
state.maxDescriptionLength = state.maxDescriptionLength.coerceIn(MAX_DESCRIPTION_LENGTH_RANGE)
update()
}
fun resetState() {
val default = State()
state.hiddenSeverities.apply { clear(); putAll(default.hiddenSeverities) }
state.showUnknownSeverities = default.showUnknownSeverities
state.useEditorFont = default.useEditorFont
state.maxDescriptionLength = default.maxDescriptionLength
state.lensHoverMode = default.lensHoverMode
update()
}

View File

@@ -14,6 +14,11 @@
]]></description>
<change-notes><![CDATA[
<b>Version 1.5.2</b>
<ul>
<li>Added option to change maximum description length.</li>
<li>Added button to <b>Settings | Tools | Inspection Lens</b> that resets all settings to default.</li>
</ul>
<b>Version 1.5.1</b>
<ul>
<li>Added option to change the behavior of clicking on inspections.</li>