From 36d0b02a0d827b74bf46cdeb49e2559e4d1817e2 Mon Sep 17 00:00:00 2001 From: Dmitry Batkovich Date: Sun, 13 Oct 2024 13:56:28 +0200 Subject: [PATCH] IJPL-164043 promote suggested refactoring using editor line hint (disabled by default, use "promote.suggested.refactoring.in.editor" adv. setting) GitOrigin-RevId: d067a744131450a2bb0945d1c847992ebb7da6d9 --- .../suggested/SuggestedRefactoringState.kt | 18 ++ .../suggested/SuggestedRefactoringUI.kt | 1 + platform/lang-impl/api-dump-unreviewed.txt | 68 ------- ...ggestedRefactoringAvailabilityIndicator.kt | 69 +++++-- .../SuggestedRefactoringEditorEnterHandler.kt | 35 ++++ ...SuggestedRefactoringEditorEscapeHandler.kt | 22 +++ ...RefactoringEditorLineHintAdvancedOption.kt | 30 +++ ...uggestedRefactoringIntentionContributor.kt | 15 +- .../refactoring/suggested/package-info.java | 5 + .../EditorLineStripeHintComponent.kt | 179 ++++++++++++++++++ .../EditorLineStripeHintRenderers.kt | 47 +++++ ...aintEditorLineStripeHintComponentAction.kt | 18 ++ .../editorLineStripeHint/package-info.java | 5 + .../src/messages/ActionsBundle.properties | 3 +- .../src/META-INF/PlatformLangComponents.xml | 3 + .../src/META-INF/PlatformLangPlugin.xml | 5 + .../src/idea/PlatformActions.xml | 1 + .../messages/RefactoringBundle.properties | 2 + 18 files changed, 427 insertions(+), 99 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorEnterHandler.kt create mode 100644 platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorEscapeHandler.kt create mode 100644 platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorLineHintAdvancedOption.kt create mode 100644 platform/lang-impl/src/com/intellij/refactoring/suggested/package-info.java create mode 100644 platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/EditorLineStripeHintComponent.kt create mode 100644 platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/EditorLineStripeHintRenderers.kt create mode 100644 platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/PaintEditorLineStripeHintComponentAction.kt create mode 100644 platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/package-info.java diff --git a/platform/lang-api/src/com/intellij/refactoring/suggested/SuggestedRefactoringState.kt b/platform/lang-api/src/com/intellij/refactoring/suggested/SuggestedRefactoringState.kt index b04585e012db..664955fef25e 100644 --- a/platform/lang-api/src/com/intellij/refactoring/suggested/SuggestedRefactoringState.kt +++ b/platform/lang-api/src/com/intellij/refactoring/suggested/SuggestedRefactoringState.kt @@ -10,6 +10,8 @@ import com.intellij.util.asSafely import com.intellij.util.keyFMap.KeyFMap import org.jetbrains.annotations.Nls import com.intellij.psi.createSmartPointer +import com.intellij.refactoring.RefactoringBundle +import org.jetbrains.annotations.ApiStatus private var nextFeatureUsageId = 0 @@ -259,3 +261,19 @@ data class SuggestedChangeSignatureData private constructor( } } } + +@ApiStatus.Internal +fun SuggestedRefactoringData.getIntentionText(): @Nls String { + val text = when (this) { + is SuggestedRenameData -> RefactoringBundle.message( + "suggested.refactoring.rename.intention.text", + oldName, + newName + ) + is SuggestedChangeSignatureData -> RefactoringBundle.message( + "suggested.refactoring.change.signature.intention.text", + nameOfStuffToUpdate + ) + } + return text +} diff --git a/platform/lang-api/src/com/intellij/refactoring/suggested/SuggestedRefactoringUI.kt b/platform/lang-api/src/com/intellij/refactoring/suggested/SuggestedRefactoringUI.kt index 4dd9d06c209e..2800a1dd86ff 100644 --- a/platform/lang-api/src/com/intellij/refactoring/suggested/SuggestedRefactoringUI.kt +++ b/platform/lang-api/src/com/intellij/refactoring/suggested/SuggestedRefactoringUI.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.ui.ValidationInfo import com.intellij.psi.PsiCodeFragment import com.intellij.refactoring.suggested.SuggestedRefactoringExecution.NewParameterValue import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Signature +import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls import javax.swing.JComponent diff --git a/platform/lang-impl/api-dump-unreviewed.txt b/platform/lang-impl/api-dump-unreviewed.txt index c2be5c246796..70d6c03b6b15 100644 --- a/platform/lang-impl/api-dump-unreviewed.txt +++ b/platform/lang-impl/api-dump-unreviewed.txt @@ -17892,74 +17892,6 @@ c:com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteUsageInfo - (com.intellij.psi.PsiElement,com.intellij.psi.PsiElement):V - (com.intellij.psi.PsiElement,com.intellij.psi.PsiElement,I,I,Z):V - getReferencedElement():com.intellij.psi.PsiElement -f:com.intellij.refactoring.suggested.NewIdentifierWatcher -- (I):V -- f:documentChanged(com.intellij.openapi.editor.event.DocumentEvent,com.intellij.lang.Language):V -- f:getLastDocument():com.intellij.openapi.editor.Document -- f:lastNewIdentifierRanges():java.util.List -- f:reset():V -f:com.intellij.refactoring.suggested.RefactoringAvailableGutterIconRenderer -- com.intellij.openapi.editor.markup.GutterIconRenderer -- (java.lang.String):V -- equals(java.lang.Object):Z -- getAlignment():com.intellij.openapi.editor.markup.GutterIconRenderer$Alignment -- getClickAction():com.intellij.openapi.actionSystem.AnAction -- getIcon():javax.swing.Icon -- getTooltipText():java.lang.String -- hashCode():I -- isNavigateAction():Z -f:com.intellij.refactoring.suggested.RefactoringDisabledGutterIconRenderer -- com.intellij.openapi.editor.markup.GutterIconRenderer -- (java.lang.String):V -- equals(java.lang.Object):Z -- getAlignment():com.intellij.openapi.editor.markup.GutterIconRenderer$Alignment -- getIcon():javax.swing.Icon -- getTooltipText():java.lang.String -- hashCode():I -f:com.intellij.refactoring.suggested.SuggestedRefactoringAvailabilityIndicator -- sf:Companion:com.intellij.refactoring.suggested.SuggestedRefactoringAvailabilityIndicator$Companion -- (com.intellij.openapi.project.Project):V -- f:clear():V -- f:disable():V -- f:getHasData():Z -- f:show(com.intellij.openapi.editor.Document,com.intellij.openapi.util.TextRange,com.intellij.openapi.util.TextRange,Z,java.lang.String):V -f:com.intellij.refactoring.suggested.SuggestedRefactoringAvailabilityIndicator$Companion -- f:getDisabledRefactoringTooltip():java.lang.String -f:com.intellij.refactoring.suggested.SuggestedRefactoringChangeCollector -- com.intellij.refactoring.suggested.SuggestedRefactoringSignatureWatcher -- (com.intellij.refactoring.suggested.SuggestedRefactoringAvailabilityIndicator):V -- editingStarted(com.intellij.psi.PsiElement,com.intellij.refactoring.suggested.SuggestedRefactoringSupport):V -- f:getState():com.intellij.refactoring.suggested.SuggestedRefactoringState -- f:get_amendStateInBackgroundEnabled():Z -- inconsistentState():V -- nextSignature(com.intellij.psi.PsiElement,com.intellij.refactoring.suggested.SuggestedRefactoringSupport):V -- reset():V -- f:set_amendStateInBackgroundEnabled(Z):V -- f:undoToState(com.intellij.refactoring.suggested.SuggestedRefactoringState):V -f:com.intellij.refactoring.suggested.SuggestedRefactoringChangeListener -- (com.intellij.openapi.project.Project,com.intellij.refactoring.suggested.SuggestedRefactoringSignatureWatcher,com.intellij.openapi.Disposable):V -- f:reset(Z):V -- bs:reset$default(com.intellij.refactoring.suggested.SuggestedRefactoringChangeListener,Z,I,java.lang.Object):V -- f:suppressForCurrentDeclaration():V -- f:undoToState(com.intellij.refactoring.suggested.SuggestedRefactoringState,com.intellij.openapi.util.TextRange):V -f:com.intellij.refactoring.suggested.SuggestedRefactoringProviderImpl -- com.intellij.refactoring.suggested.SuggestedRefactoringProvider -- sf:Companion:com.intellij.refactoring.suggested.SuggestedRefactoringProviderImpl$Companion -- (com.intellij.openapi.project.Project):V -- f:getAvailabilityIndicator():com.intellij.refactoring.suggested.SuggestedRefactoringAvailabilityIndicator -- f:getState():com.intellij.refactoring.suggested.SuggestedRefactoringState -- f:get_amendStateInBackgroundEnabled():Z -- reset():V -- f:set_amendStateInBackgroundEnabled(Z):V -- f:suppressForCurrentDeclaration():V -- f:undoToState(com.intellij.refactoring.suggested.SuggestedRefactoringState,com.intellij.openapi.util.TextRange):V -f:com.intellij.refactoring.suggested.SuggestedRefactoringProviderImpl$Companion -- f:getInstance(com.intellij.openapi.project.Project):com.intellij.refactoring.suggested.SuggestedRefactoringProviderImpl -com.intellij.refactoring.suggested.SuggestedRefactoringSignatureWatcher -- a:editingStarted(com.intellij.psi.PsiElement,com.intellij.refactoring.suggested.SuggestedRefactoringSupport):V -- a:inconsistentState():V -- a:nextSignature(com.intellij.psi.PsiElement,com.intellij.refactoring.suggested.SuggestedRefactoringSupport):V -- a:reset():V a:com.intellij.refactoring.ui.AbstractMemberSelectionPanel - javax.swing.JPanel - ():V diff --git a/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringAvailabilityIndicator.kt b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringAvailabilityIndicator.kt index 9c3aed5f3fd9..c2841102ec8b 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringAvailabilityIndicator.kt +++ b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringAvailabilityIndicator.kt @@ -2,6 +2,9 @@ package com.intellij.refactoring.suggested import com.intellij.codeInsight.daemon.GutterMark +import com.intellij.codeInsight.editorLineStripeHint.EditorLineStripeButtonRenderer +import com.intellij.codeInsight.editorLineStripeHint.EditorLineStripeHintComponent +import com.intellij.codeInsight.editorLineStripeHint.EditorLineStripeTextRenderer import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.* import com.intellij.openapi.application.ApplicationManager @@ -14,12 +17,15 @@ import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.markup.* import com.intellij.openapi.keymap.KeymapUtil import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.RefactoringCodeVisionSupport +import com.intellij.ui.JBColor import com.intellij.util.concurrency.ThreadingAssertions import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls @@ -32,7 +38,8 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { val highlighterRangeMarker: RangeMarker, val availabilityRangeMarker: RangeMarker, val refactoringEnabled: Boolean, - @NlsContexts.Tooltip val tooltip: String + @NlsContexts.Tooltip val tooltip: String, + @Nls val intentionText: String?, ) { override fun equals(other: Any?): Boolean { return other is Data @@ -41,6 +48,7 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { && other.availabilityRangeMarker.asTextRange == availabilityRangeMarker.asTextRange && other.refactoringEnabled == refactoringEnabled && other.tooltip == tooltip + && other.intentionText == intentionText } override fun hashCode() = tooltip.hashCode() @@ -48,7 +56,7 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { private var data: Data? = null - private val editorsAndHighlighters = mutableMapOf() + private val editorsAndHighlighters = mutableMapOf>() private val caretListener = object : CaretListener { override fun caretPositionChanged(event: CaretEvent) { @@ -73,7 +81,8 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { markerRange: TextRange, availabilityRange: TextRange, refactoringEnabled: Boolean, - @NlsContexts.Tooltip tooltip: String + @NlsContexts.Tooltip tooltip: String, + @Nls intentionText: String?, ) { ThreadingAssertions.assertEventDispatchThread() @@ -82,7 +91,8 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { document.createRangeMarker(markerRange), document.createRangeMarker(availabilityRange).apply { isGreedyToLeft = true; isGreedyToRight = true }, refactoringEnabled, - tooltip + tooltip, + intentionText ) if (newData == data) return @@ -98,13 +108,21 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { if (data == null) return data = null - for ((editor, highlighter) in editorsAndHighlighters) { + for ((editor, highlighterAndHint) in editorsAndHighlighters) { editor.caretModel.removeCaretListener(caretListener) - highlighter?.let { editor.markupModel.removeHighlighter(it) } + editor.markupModel.removeHighlighter(highlighterAndHint.first) + highlighterAndHint.second?.let { hint -> + hint.uninstall() + Disposer.dispose(hint) + } } editorsAndHighlighters.clear() } + internal fun isHintShown(editor: Editor): Boolean { + return editorsAndHighlighters[editor]?.second != null + } + fun disable() { ThreadingAssertions.assertEventDispatchThread() val data = data ?: return @@ -114,7 +132,8 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { data.highlighterRangeMarker.asTextRange ?: return, data.availabilityRangeMarker.asTextRange ?: return, false, - data.tooltip + data.tooltip, + data.intentionText, ) } } @@ -133,15 +152,20 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { private fun updateHighlighter(editor: Editor) { ThreadingAssertions.assertEventDispatchThread() - val prevHighlighter = editorsAndHighlighters[editor] - if (prevHighlighter != null) { - editor.markupModel.removeHighlighter(prevHighlighter) + val prevHighlighterAndHint = editorsAndHighlighters[editor] + if (prevHighlighterAndHint != null) { + editor.markupModel.removeHighlighter(prevHighlighterAndHint.first) editorsAndHighlighters.remove(editor) + prevHighlighterAndHint.second?.let { hint -> + hint.uninstall() + Disposer.dispose(hint) + } } + val data = data val range = data?.availabilityRangeMarker?.asTextRange ?: return if (!range.containsOffset(editor.caretModel.offset)) return - val highlighterRange = data!!.highlighterRangeMarker.asTextRange ?: return + val highlighterRange = data.highlighterRangeMarker.asTextRange ?: return val highlighter = editor.markupModel.addRangeHighlighter( highlighterRange.startOffset, @@ -150,11 +174,22 @@ class SuggestedRefactoringAvailabilityIndicator(private val project: Project) { TextAttributes(), HighlighterTargetArea.EXACT_RANGE ) - highlighter.gutterIconRenderer = if (data!!.refactoringEnabled) - RefactoringAvailableGutterIconRenderer(data!!.tooltip) - else - RefactoringDisabledGutterIconRenderer(data!!.tooltip) - editorsAndHighlighters[editor] = highlighter + + var hint: EditorLineStripeHintComponent? = null + if (data.refactoringEnabled) { + highlighter.gutterIconRenderer = RefactoringAvailableGutterIconRenderer(data.tooltip) + if (data.intentionText != null && isSuggestedRefactoringEditorHintEnabled()) { + hint = EditorLineStripeHintComponent(editor, { + listOf(listOf(EditorLineStripeTextRenderer(data.intentionText), EditorLineStripeButtonRenderer("Enter"))) + }, JBColor.GREEN) + hint.redraw() + } + } + else { + highlighter.gutterIconRenderer = RefactoringDisabledGutterIconRenderer(data.tooltip) + } + + editorsAndHighlighters[editor] = highlighter to hint } companion object { @@ -261,7 +296,7 @@ internal fun SuggestedRefactoringAvailabilityIndicator.update( fun doUpdate() { if (availabilityRange != null) { - show(document, markerRange, availabilityRange, refactoringAvailable, tooltip) + show(document, markerRange, availabilityRange, refactoringAvailable, tooltip, refactoringData?.getIntentionText()) } else { clear() diff --git a/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorEnterHandler.kt b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorEnterHandler.kt new file mode 100644 index 000000000000..60fa602f45e2 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorEnterHandler.kt @@ -0,0 +1,35 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.refactoring.suggested + +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.Project + +private class SuggestedRefactoringEditorEnterHandler(private val originalHandler: EditorActionHandler) : EditorActionHandler() { + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + val project = editor.project + return isSuggestedRefactoringHintShown(project, editor) || originalHandler.isEnabled(editor, caret, dataContext) + } + + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext) { + val project = editor.project + if (isSuggestedRefactoringHintShown(project, editor)) { + performSuggestedRefactoring(project!!, + editor, + null, + null, + showReviewBalloon = true, + ActionPlaces.KEYBOARD_SHORTCUT) + } + else { + originalHandler.execute(editor, caret, dataContext) + } + } +} + +internal fun isSuggestedRefactoringHintShown(project: Project?, editor: Editor): Boolean = project != null && + isSuggestedRefactoringEditorHintEnabled() && + SuggestedRefactoringProviderImpl.getInstance(project).availabilityIndicator.isHintShown(editor) \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorEscapeHandler.kt b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorEscapeHandler.kt new file mode 100644 index 000000000000..6c15ab60c3e1 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorEscapeHandler.kt @@ -0,0 +1,22 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.refactoring.suggested + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorActionHandler + +private class SuggestedRefactoringEditorEscapeHandler(private val originalHandler: EditorActionHandler) : EditorActionHandler() { + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + val project = editor.project + return isSuggestedRefactoringHintShown(project, editor) || originalHandler.isEnabled(editor, caret, dataContext) + } + + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext) { + val project = editor.project + if (isSuggestedRefactoringHintShown(project, editor)) { + SuggestedRefactoringProviderImpl.getInstance(project!!).suppressForCurrentDeclaration() + } + originalHandler.execute(editor, caret, dataContext) + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorLineHintAdvancedOption.kt b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorLineHintAdvancedOption.kt new file mode 100644 index 000000000000..84029739fd6a --- /dev/null +++ b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringEditorLineHintAdvancedOption.kt @@ -0,0 +1,30 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.refactoring.suggested + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.openapi.options.advanced.AdvancedSettings +import com.intellij.openapi.options.advanced.AdvancedSettingsChangeListener +import com.intellij.openapi.project.ProjectManager + +private const val ADV_SETTING_NAME = "promote.suggested.refactoring.in.editor" + +internal fun isSuggestedRefactoringEditorHintEnabled(): Boolean { + return AdvancedSettings.getBoolean(ADV_SETTING_NAME) +} + +private class SuggestedRefactoringEditorHintAdvSettingListener: AdvancedSettingsChangeListener { + override fun advancedSettingChanged(id: String, oldValue: Any, newValue: Any) { + if (ADV_SETTING_NAME == id) { + if (newValue == true) { + ProjectManager.getInstance().openProjects.forEach { + DaemonCodeAnalyzer.getInstance(it).restart() + } + } + else { + ProjectManager.getInstance().openProjects.asSequence().forEach { + SuggestedRefactoringProviderImpl.getInstance(it).suppressForCurrentDeclaration() + } + } + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringIntentionContributor.kt b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringIntentionContributor.kt index 78b2579c98fd..d31073693b8e 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringIntentionContributor.kt +++ b/platform/lang-impl/src/com/intellij/refactoring/suggested/SuggestedRefactoringIntentionContributor.kt @@ -20,6 +20,7 @@ import com.intellij.psi.PsiFile import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.suggested.SuggestedRefactoringState.ErrorLevel import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls internal val REFACTORING_DATA_KEY: Key = Key.create("suggested.refactoring.data") @@ -100,19 +101,7 @@ class SuggestedRefactoringIntentionContributor : IntentionMenuContributor { if (!range.containsOffset(offset)) return null SuggestedRefactoringFeatureUsage.refactoringSuggested(refactoringData, state) - - val text = when (refactoringData) { - is SuggestedRenameData -> RefactoringBundle.message( - "suggested.refactoring.rename.intention.text", - refactoringData.oldName, - refactoringData.newName - ) - - is SuggestedChangeSignatureData -> RefactoringBundle.message( - "suggested.refactoring.change.signature.intention.text", - refactoringData.nameOfStuffToUpdate - ) - } + val text = refactoringData.getIntentionText() return MyIntention(text, showReviewBalloon = refactoringData is SuggestedChangeSignatureData) } diff --git a/platform/lang-impl/src/com/intellij/refactoring/suggested/package-info.java b/platform/lang-impl/src/com/intellij/refactoring/suggested/package-info.java new file mode 100644 index 000000000000..765a5371157d --- /dev/null +++ b/platform/lang-impl/src/com/intellij/refactoring/suggested/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@ApiStatus.Internal +package com.intellij.refactoring.suggested; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/EditorLineStripeHintComponent.kt b/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/EditorLineStripeHintComponent.kt new file mode 100644 index 000000000000..83353c36481d --- /dev/null +++ b/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/EditorLineStripeHintComponent.kt @@ -0,0 +1,179 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.editorLineStripeHint + +import com.intellij.codeInsight.daemon.impl.HintRenderer +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.VisibleAreaEvent +import com.intellij.openapi.editor.event.VisibleAreaListener +import com.intellij.openapi.editor.impl.EditorImpl +import com.intellij.openapi.observable.util.whenDisposed +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBPanel +import java.awt.Color +import java.awt.Component +import java.awt.Dimension +import java.awt.GradientPaint +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.Rectangle +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import javax.swing.BoxLayout +import javax.swing.JPanel + +class EditorLineStripeHintComponent( + val editor: Editor, + panelRenderer: () -> List>, + val stripeColor: JBColor, +) : JBPanel>(), Disposable { + @Suppress("UseJBColor") + val gradientStartColor: Color = run { + // transparent + Color(0, 0, 0, 0) + } + + private val isInstalled = AtomicBoolean(false) + private val lifetime: AtomicInteger = AtomicInteger(4) + + private val batches: List> + + init { + isOpaque = false + layout = BoxLayout(this, BoxLayout.X_AXIS) + batches = panelRenderer().map { renderers -> + renderers.map { renderer -> PhantomInlayComponent(editor, renderer) } + } + + for (batch in batches) { + batch.forEach { add(it) } + } + + val editorListener = EditorListener(this) + editor.contentComponent.addComponentListener(editorListener) + editor.scrollingModel.addVisibleAreaListener(editorListener) + + whenDisposed { + editor.contentComponent.removeComponentListener(editorListener) + editor.scrollingModel.removeVisibleAreaListener(editorListener) + } + } + + fun reposition() { + val width = editor.scrollingModel.visibleArea.width + setSize(width / 2, editor.lineHeight) + ApplicationManager.getApplication().runReadAction { + val caretPoint = editor.visualPositionToXY(editor.caretModel.visualPosition) + val startPositionOfEolStripe = (width / 3) * 2 + setBounds(editor.contentComponent.visibleRect.x + (width / 2), caretPoint.y, width / 2, editor.lineHeight) + for (comp in components) { + if (comp is PhantomInlayComponent) { + comp.reposition() + } + } + val lineEndOffset = editor.document.getLineEndOffset(editor.caretModel.logicalPosition.line) + val caretOffset = editor.caretModel.offset + val inlays = editor.inlayModel.getAfterLineEndElementsInRange(caretOffset, lineEndOffset) + val meaningfulTextEnd = editor.offsetToXY(lineEndOffset).x + inlays.sumOf { it.widthInPixels } + var remainingSpace = if (meaningfulTextEnd > startPositionOfEolStripe) { + -1 + } + else { + editor.component.size.width - meaningfulTextEnd + //size.width + } + for (batch in batches) { + val compoundSize = batch.sumOf { it.maximumSize.width } + if (compoundSize < remainingSpace) { + batch.forEach { it.isVisible = true } + remainingSpace -= compoundSize + } + else { + batch.forEach { it.isVisible = false } + remainingSpace -= compoundSize // yes, can be negative. In this case everything to the right is hidden + } + } + } + } + + fun isShown(): Boolean = isInstalled.get() + + fun redraw() { + uninstall() + val currentLifetime = lifetime.getAndDecrement() + if (currentLifetime <= 0) { + lifetime.set(0) + return + } + reposition() + install() + } + + private fun install() { + if (isInstalled.compareAndSet(false, true)) { + editor.contentComponent.add(this) + } + } + + fun uninstall() { + if (isInstalled.compareAndSet(true, false)) { + editor.contentComponent.remove(this) + } + } + + override fun paintComponent(g: Graphics?) { + super.paintComponent(g) + val g2d = g as Graphics2D + val width = width + val height = height + val gradient = GradientPaint((width.toFloat() / 3) * 2, height / 2f, gradientStartColor, width.toFloat(), height.toFloat() / 2f, stripeColor) + g2d.paint = gradient + g2d.fillRect(0, 0, width, height) + } + + override fun dispose(): Unit = Unit + + class PhantomInlayComponent(val editor: Editor, val renderer: EditorLineStripeInlayRenderer) : JPanel() { + var pixelWidth = HintRenderer.Companion.calcWidthInPixels(editor, renderer.text, renderer.widthAdjustment) + + init { + reposition() + } + + fun reposition() { + pixelWidth = HintRenderer.Companion.calcWidthInPixels(editor, renderer.text, renderer.widthAdjustment) + val delta = if (renderer is EditorLineStripeButtonRenderer) 3 else -5 + maximumSize = Dimension(pixelWidth - delta, editor.lineHeight) + // delta is needed so that text is close to the button, but far from other text + } + + override fun paint(g: Graphics) { + val attrs = renderer.internalGetTextAttributes(editor) + ?: editor.colorsScheme.getAttributes(DefaultLanguageHighlighterColors.INLINE_PARAMETER_HINT) + + HintRenderer.Companion.paintHint( + g, + editor as EditorImpl, + // todo: figure out why 0 is here + Rectangle(0, y, pixelWidth, height), + renderer.text, + attrs, + attrs, + renderer.widthAdjustment, false) + } + } +} + +private class EditorListener(val panel: EditorLineStripeHintComponent) : ComponentAdapter(), VisibleAreaListener { + override fun componentResized(e: ComponentEvent) { + panel.reposition() + } + + override fun visibleAreaChanged(e: VisibleAreaEvent) { + panel.reposition() + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/EditorLineStripeHintRenderers.kt b/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/EditorLineStripeHintRenderers.kt new file mode 100644 index 000000000000..06d2bf414713 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/EditorLineStripeHintRenderers.kt @@ -0,0 +1,47 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.editorLineStripeHint + +import com.intellij.codeInsight.daemon.impl.HintRenderer +import com.intellij.codeInsight.hints.presentation.InputHandler +import com.intellij.codeInsight.inline.completion.InlineCompletionFontUtils +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.markup.TextAttributes +import org.jetbrains.annotations.Nls +import java.awt.Graphics +import java.awt.Rectangle + +sealed class EditorLineStripeInlayRenderer(text: String) : HintRenderer(text) { + // exposes protected method in HintRenderer to the current package + internal fun internalGetTextAttributes(editor: Editor): TextAttributes? = getTextAttributes(editor) +} + +class EditorLineStripeButtonRenderer(text: String) : EditorLineStripeInlayRenderer(text) { + override fun toString(): String { + return "<|$text|>" + } +} + +class EditorLineStripeTextRenderer(text: @Nls String) : EditorLineStripeInlayRenderer(text), InputHandler { + override fun getTextAttributes(editor: Editor): TextAttributes? { + return super.getTextAttributes(editor)?.clearEffects(editor) + } + + private fun TextAttributes.clearEffects(editor: Editor): TextAttributes = clone().apply { + effectType = null + effectColor = null + backgroundColor = null + foregroundColor = InlineCompletionFontUtils.color(editor) + setAdditionalEffects(emptyMap()) + } + + override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) { + // shift because we want two independent inlays to looks related + val shiftedRectangle = Rectangle(r.x - 5, r.y, r.width, r.height) + super.paint(inlay, g, shiftedRectangle, textAttributes) + } + + override fun toString(): String { + return "<#$text#>" + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/PaintEditorLineStripeHintComponentAction.kt b/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/PaintEditorLineStripeHintComponentAction.kt new file mode 100644 index 000000000000..2755570f9939 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/PaintEditorLineStripeHintComponentAction.kt @@ -0,0 +1,18 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.editorLineStripeHint + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.PlatformCoreDataKeys +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.ui.JBColor + +private class PaintEditorLineStripeHintComponentAction : DumbAwareAction() { + @Suppress("HardCodedStringLiteral") + override fun actionPerformed(e: AnActionEvent) { + val editor = e.dataContext.getData(PlatformCoreDataKeys.EDITOR) ?: return + EditorLineStripeHintComponent(editor, { + listOf(listOf(EditorLineStripeTextRenderer("Component_1"), EditorLineStripeButtonRenderer("Button_1")), + listOf(EditorLineStripeTextRenderer("Component_2"), EditorLineStripeButtonRenderer("Button_2"))) + }, JBColor.BLUE).redraw() + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/package-info.java b/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/package-info.java new file mode 100644 index 000000000000..938ce9904b68 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/codeInsight/editorLineStripeHint/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@ApiStatus.Internal +package com.intellij.codeInsight.editorLineStripeHint; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties index cbcd687f424b..dfb3fd5b8b9d 100644 --- a/platform/platform-resources-en/src/messages/ActionsBundle.properties +++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties @@ -2928,4 +2928,5 @@ action.ToolWindowShowNamesAction.text=Show Tool Window Names action.TogglePresentationAssistantAction.text=Presentation Assistant action.separator=Separator -action.ConfigureInspectionsAction.text=Configure Inspections\u2026 \ No newline at end of file +action.ConfigureInspectionsAction.text=Configure Inspections\u2026 +action.PaintEditorLineStripeComponentAction.text=Paint Editor Caret Stripe \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/PlatformLangComponents.xml b/platform/platform-resources/src/META-INF/PlatformLangComponents.xml index 5d70723c10eb..080cdf96989b 100644 --- a/platform/platform-resources/src/META-INF/PlatformLangComponents.xml +++ b/platform/platform-resources/src/META-INF/PlatformLangComponents.xml @@ -253,6 +253,9 @@ topic="com.intellij.ide.AppLifecycleListener" activeInHeadlessMode="true" activeInTestMode="false" /> + + + + + diff --git a/platform/platform-resources/src/idea/PlatformActions.xml b/platform/platform-resources/src/idea/PlatformActions.xml index 11bf05b4b148..d71eba2261ca 100644 --- a/platform/platform-resources/src/idea/PlatformActions.xml +++ b/platform/platform-resources/src/idea/PlatformActions.xml @@ -1755,5 +1755,6 @@ + diff --git a/platform/refactoring/resources/messages/RefactoringBundle.properties b/platform/refactoring/resources/messages/RefactoringBundle.properties index ad06877c99c0..c7b24a1c9e6e 100644 --- a/platform/refactoring/resources/messages/RefactoringBundle.properties +++ b/platform/refactoring/resources/messages/RefactoringBundle.properties @@ -515,3 +515,5 @@ progress.title.collecting.suggested.names=Collecting suggested names dialog.message.selected.element.used.from.non.project.files=Selected element is used from non-project files. These usages won't be renamed. Proceed anyway? progress.title.prepare.to.delete=Prepare to delete progress.title.prepare.additional.searcher=Prepare additional searcher +advanced.setting.promote.suggested.refactoring.in.editor=Show a hint for suggested refactoring in the editor +suggested.refactoring.group.key=Suggested refactoring