mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-15 02:59:33 +07:00
[spellchecker] IJPL-177881 "Fix typo" in a key in .properties file should invoke "Rename" refactoring instead of just fixing the typo
Merge-request: IJ-MR-165210 Merged-by: Ilia Permiashkin <ilia.permiashkin@jetbrains.com> GitOrigin-RevId: 9aa1e7d8a4b296936e1994e86268ffdfa64d28ef
This commit is contained in:
committed by
intellij-monorepo-bot
parent
36831d7c33
commit
bfc4a9be16
@@ -2,7 +2,7 @@
|
||||
package org.jetbrains.idea.devkit.spellchecker
|
||||
|
||||
import com.intellij.lang.properties.psi.impl.PropertyValueImpl
|
||||
import com.intellij.lang.properties.spellchecker.MnemonicsTokenizer
|
||||
import com.intellij.lang.properties.spellchecker.tokenizer.MnemonicsTokenizer
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleUtilCore
|
||||
import com.intellij.openapi.project.IntelliJProjectUtil.isIntelliJPlatformProject
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
interface="com.intellij.lang.properties.codeInspection.unused.ExtendedUseScopeProvider"
|
||||
dynamic="true"/>
|
||||
<extensionPoint qualifiedName="com.intellij.properties.spellcheckerMnemonicsTokenizer"
|
||||
interface="com.intellij.lang.properties.spellchecker.MnemonicsTokenizer"
|
||||
interface="com.intellij.lang.properties.spellchecker.tokenizer.MnemonicsTokenizer"
|
||||
dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
|
||||
<spellchecker.support language="Properties"
|
||||
id="propertiesSpellcheckingStrategy"
|
||||
implementationClass="com.intellij.lang.properties.spellchecker.PropertiesSpellcheckingStrategy"/>
|
||||
implementationClass="com.intellij.lang.properties.spellchecker.tokenizer.PropertiesSpellcheckingStrategy"/>
|
||||
<spellchecker.renamer implementation="com.intellij.lang.properties.spellchecker.handler.PropertySpellcheckingHandler"/>
|
||||
|
||||
<fileBasedIndex implementation="com.intellij.lang.properties.xml.XmlPropertiesIndex"/>
|
||||
<standardResource url="http://java.sun.com/dtd/properties.dtd" path="schemas/properties.dtd"/>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.intellij.lang.properties.spellchecker.handler
|
||||
|
||||
import com.intellij.lang.properties.psi.impl.PropertyKeyImpl
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiNamedElement
|
||||
import com.intellij.spellchecker.handler.SpellcheckingElementHandler
|
||||
|
||||
class PropertySpellcheckingHandler : SpellcheckingElementHandler {
|
||||
|
||||
override fun isEligibleForRenaming(psiElement: PsiElement): Boolean = psiElement is PropertyKeyImpl
|
||||
|
||||
override fun getNamedElement(psiElement: PsiElement): PsiNamedElement? = psiElement.parent as PsiNamedElement
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.lang.properties.spellchecker;
|
||||
package com.intellij.lang.properties.spellchecker.tokenizer;
|
||||
|
||||
import com.intellij.lang.properties.psi.impl.PropertyValueImpl;
|
||||
import com.intellij.spellchecker.tokenizer.TokenConsumer;
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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.lang.properties.spellchecker;
|
||||
package com.intellij.lang.properties.spellchecker.tokenizer;
|
||||
|
||||
import com.intellij.codeInsight.CodeInsightUtilCore;
|
||||
import com.intellij.lang.properties.psi.Property;
|
||||
@@ -16,6 +16,7 @@
|
||||
<extensionPoint name="spellchecker.dictionaryLayersProvider" interface="com.intellij.spellchecker.DictionaryLayersProvider" dynamic="true"/>
|
||||
<extensionPoint name="spellchecker.quickFixFactory" interface="com.intellij.spellchecker.quickfixes.SpellCheckerQuickFixFactory" dynamic="true"/>
|
||||
<extensionPoint name="spellchecker.lifecycle" interface="com.intellij.spellchecker.grazie.SpellcheckerLifecycle" dynamic="true"/>
|
||||
<extensionPoint name="spellchecker.renamer" interface="com.intellij.spellchecker.handler.SpellcheckingElementHandler" dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.spellchecker.handler
|
||||
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiNamedElement
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
@ApiStatus.Experimental
|
||||
interface SpellcheckingElementHandler {
|
||||
|
||||
/**
|
||||
* Determines whether the given `psiElement` is eligible for renaming.
|
||||
*
|
||||
* @param psiElement the PSI element to be checked for renaming eligibility
|
||||
* @return `true` if the provided element can be renamed, otherwise `false`
|
||||
*/
|
||||
fun isEligibleForRenaming(psiElement: PsiElement): Boolean
|
||||
|
||||
/**
|
||||
* Returns the [PsiNamedElement] associated with the given `psiElement`, if any.
|
||||
*
|
||||
* @param psiElement the PSI element to get the named element for
|
||||
* @return the named element associated with the given `psiElement`, or `null` if no named element is found
|
||||
*/
|
||||
fun getNamedElement(psiElement: PsiElement): PsiNamedElement?
|
||||
}
|
||||
@@ -6,10 +6,16 @@ import com.intellij.codeInsight.intention.HighPriorityAction
|
||||
import com.intellij.codeInsight.intention.choice.ChoiceTitleIntentionAction
|
||||
import com.intellij.codeInsight.intention.choice.ChoiceVariantIntentionAction
|
||||
import com.intellij.codeInsight.intention.choice.DefaultIntentionActionWithChoice
|
||||
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
|
||||
import com.intellij.codeInsight.intention.preview.IntentionPreviewUtils
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.writeIntentReadAction
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.impl.DocumentMarkupModel
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.progress.currentThreadCoroutineScope
|
||||
import com.intellij.openapi.progress.withCurrentThreadCoroutineScopeBlocking
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
@@ -20,13 +26,19 @@ import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.SmartPointerManager
|
||||
import com.intellij.psi.util.startOffset
|
||||
import com.intellij.refactoring.rename.PsiElementRenameHandler
|
||||
import com.intellij.spellchecker.handler.SpellcheckingElementHandler
|
||||
import com.intellij.spellchecker.util.SpellCheckerBundle
|
||||
import com.intellij.util.concurrency.ThreadingAssertions
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class ChangeTo(typo: String, element: PsiElement, private val range: TextRange) : DefaultIntentionActionWithChoice, LazySuggestions(typo) {
|
||||
private val pointer = SmartPointerManager.getInstance(element.project).createSmartPsiElementPointer(element, element.containingFile)
|
||||
|
||||
companion object {
|
||||
private val EP_NAME = ExtensionPointName.create<SpellcheckingElementHandler>("com.intellij.spellchecker.renamer")
|
||||
|
||||
@JvmStatic
|
||||
val fixName: String by lazy {
|
||||
SpellCheckerBundle.message("change.to.title")
|
||||
@@ -37,9 +49,9 @@ internal class ChangeTo(typo: String, element: PsiElement, private val range: Te
|
||||
|
||||
override fun getTitle(): ChoiceTitleIntentionAction = ChangeToTitleAction
|
||||
|
||||
private inner class ChangeToVariantAction(
|
||||
override val index: Int
|
||||
) : ChoiceVariantIntentionAction(), HighPriorityAction, DumbAware {
|
||||
private open inner class ChangeToVariantAction(
|
||||
override val index: Int,
|
||||
) : ChoiceVariantIntentionAction(), HighPriorityAction {
|
||||
|
||||
@NlsSafe
|
||||
private var suggestion: String? = null
|
||||
@@ -60,15 +72,43 @@ internal class ChangeTo(typo: String, element: PsiElement, private val range: Te
|
||||
|
||||
override fun applyFix(project: Project, psiFile: PsiFile, editor: Editor?) {
|
||||
val suggestion = suggestion ?: return
|
||||
|
||||
val document = psiFile.viewProvider.document
|
||||
val myRange = getRange(document) ?: return
|
||||
|
||||
removeHighlightersWithExactRange(document, project, myRange)
|
||||
pointer.element?.let { element ->
|
||||
getElementHandler(element)?.let { handler ->
|
||||
val typo = document.text.substring(range.startOffset, range.endOffset)
|
||||
val value = element.text.replace(typo, suggestion)
|
||||
|
||||
handler.getNamedElement(element)?.let { namedElement ->
|
||||
runOnEdt {
|
||||
if (namedElement.isValid) {
|
||||
PsiElementRenameHandler.rename(namedElement, project, namedElement, editor, value)
|
||||
}
|
||||
}
|
||||
return@applyFix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeHighlightersWithExactRange(document, project, myRange)
|
||||
document.replaceString(myRange.startOffset, myRange.endOffset, suggestion)
|
||||
}
|
||||
|
||||
private fun getElementHandler(element: PsiElement): SpellcheckingElementHandler? {
|
||||
return EP_NAME.extensionList.asSequence().filter { it.isEligibleForRenaming(element) }.firstOrNull()
|
||||
}
|
||||
|
||||
fun runOnEdt(runnable: Runnable) {
|
||||
withCurrentThreadCoroutineScopeBlocking {
|
||||
currentThreadCoroutineScope().launch(Dispatchers.EDT) {
|
||||
writeIntentReadAction {
|
||||
runnable.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRange(document: Document): TextRange? {
|
||||
val element = pointer.element ?: return null
|
||||
val range = range.shiftRight(element.startOffset)
|
||||
@@ -80,10 +120,23 @@ internal class ChangeTo(typo: String, element: PsiElement, private val range: Te
|
||||
}
|
||||
|
||||
override fun getFileModifierForPreview(target: PsiFile): FileModifier {
|
||||
return this
|
||||
return ForPreview(index)
|
||||
}
|
||||
|
||||
override fun startInWriteAction(): Boolean = true
|
||||
|
||||
private inner class ForPreview(
|
||||
index: Int,
|
||||
) : ChangeToVariantAction(index = index), IntentionPreviewInfo {
|
||||
override fun applyFix(project: Project, psiFile: PsiFile, editor: Editor?) {
|
||||
val suggestion = suggestion ?: return
|
||||
val document = psiFile.viewProvider.document
|
||||
val myRange = getRange(document) ?: return
|
||||
|
||||
removeHighlightersWithExactRange(document, project, myRange)
|
||||
document.replaceString(myRange.startOffset, myRange.endOffset, suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,8 +160,8 @@ internal class ChangeTo(typo: String, element: PsiElement, private val range: Te
|
||||
val model = DocumentMarkupModel.forDocument(document, project, false) ?: return
|
||||
|
||||
for (highlighter in model.allHighlighters) {
|
||||
if (TextRange.areSegmentsEqual(range, highlighter!!)) {
|
||||
model.removeHighlighter(highlighter!!)
|
||||
if (TextRange.areSegmentsEqual(range, highlighter)) {
|
||||
model.removeHighlighter(highlighter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user