mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-14 18:05:27 +07:00
IJPL-177881 "Fix typo" in a key in .properties file should invoke "Rename" refactoring instead of just fixing the typo
Merge-request: IJ-MR-166998 Merged-by: Ilia Permiashkin <ilia.permiashkin@jetbrains.com> GitOrigin-RevId: 2dd0def508db09aa5fad66c294de719c6f2f306b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
bb50516f9d
commit
13e74e6862
@@ -2,7 +2,7 @@
|
||||
package org.jetbrains.idea.devkit.spellchecker
|
||||
|
||||
import com.intellij.lang.properties.psi.impl.PropertyValueImpl
|
||||
import com.intellij.lang.properties.spellchecker.tokenizer.MnemonicsTokenizer
|
||||
import com.intellij.lang.properties.spellchecker.MnemonicsTokenizer
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleUtilCore
|
||||
import com.intellij.openapi.project.IntelliJProjectUtil.isIntelliJPlatformProject
|
||||
|
||||
@@ -27,6 +27,7 @@ jvm_library(
|
||||
"//platform/code-style-impl:codeStyle-impl",
|
||||
"//platform/lang-impl",
|
||||
"//platform/platform-impl:ide-impl",
|
||||
"//platform/refactoring",
|
||||
],
|
||||
runtime_deps = [":properties_resources"]
|
||||
)
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.codeStyle.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.refactoring" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -22,6 +22,7 @@
|
||||
<langCodeStyleSettingsProvider implementation="com.intellij.lang.properties.codeStyle.PropertiesLanguageCodeStyleSettingsProvider"/>
|
||||
<lang.ast.factory language="Properties" implementationClass="com.intellij.lang.properties.psi.impl.PropertiesASTFactory"/>
|
||||
<enterHandlerDelegate implementation="com.intellij.lang.properties.EnterInPropertiesFileHandler" id="EnterInPropertiesFileHandler"/>
|
||||
<renameInputValidator implementation="com.intellij.lang.properties.rename.PropertyKeyRenameInputValidator"/>
|
||||
|
||||
<stripTrailingSpacesFilterFactory implementation="com.intellij.lang.properties.formatting.PropertiesStripTrailingSpacesFilterFactory"/>
|
||||
</extensions>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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.rename
|
||||
|
||||
import com.intellij.lang.properties.psi.impl.PropertyKeyImpl
|
||||
import com.intellij.patterns.ElementPattern
|
||||
import com.intellij.patterns.PlatformPatterns
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.refactoring.rename.RenameInputValidator
|
||||
import com.intellij.util.ProcessingContext
|
||||
|
||||
class PropertyKeyRenameInputValidator: RenameInputValidator {
|
||||
private val myPattern: ElementPattern<out PsiElement?> = PlatformPatterns.psiElement(PropertyKeyImpl::class.java)
|
||||
|
||||
override fun getPattern(): ElementPattern<out PsiElement?> = myPattern
|
||||
|
||||
override fun isInputValid(newName: String, element: PsiElement, context: ProcessingContext): Boolean = !newName.contains(' ')
|
||||
}
|
||||
@@ -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.tokenizer.MnemonicsTokenizer"
|
||||
interface="com.intellij.lang.properties.spellchecker.MnemonicsTokenizer"
|
||||
dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
@@ -105,8 +105,7 @@
|
||||
|
||||
<spellchecker.support language="Properties"
|
||||
id="propertiesSpellcheckingStrategy"
|
||||
implementationClass="com.intellij.lang.properties.spellchecker.tokenizer.PropertiesSpellcheckingStrategy"/>
|
||||
<spellchecker.renamer implementation="com.intellij.lang.properties.spellchecker.handler.PropertySpellcheckingHandler"/>
|
||||
implementationClass="com.intellij.lang.properties.spellchecker.PropertiesSpellcheckingStrategy"/>
|
||||
|
||||
<fileBasedIndex implementation="com.intellij.lang.properties.xml.XmlPropertiesIndex"/>
|
||||
<standardResource url="http://java.sun.com/dtd/properties.dtd" path="schemas/properties.dtd"/>
|
||||
|
||||
@@ -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.tokenizer;
|
||||
package com.intellij.lang.properties.spellchecker;
|
||||
|
||||
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.tokenizer;
|
||||
package com.intellij.lang.properties.spellchecker;
|
||||
|
||||
import com.intellij.codeInsight.CodeInsightUtilCore;
|
||||
import com.intellij.lang.properties.psi.Property;
|
||||
@@ -10,6 +10,7 @@ import com.intellij.openapi.project.DumbAware;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.spellchecker.inspections.PlainTextSplitter;
|
||||
import com.intellij.spellchecker.inspections.PropertiesSplitter;
|
||||
import com.intellij.spellchecker.inspections.Splitter;
|
||||
import com.intellij.spellchecker.tokenizer.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -19,7 +20,7 @@ final class PropertiesSpellcheckingStrategy extends SpellcheckingStrategy implem
|
||||
ExtensionPointName.create("com.intellij.properties.spellcheckerMnemonicsTokenizer");
|
||||
|
||||
private final Tokenizer<PropertyValueImpl> myPropertyValueTokenizer = new PropertyValueTokenizer();
|
||||
private final Tokenizer<PropertyKeyImpl> myPropertyTokenizer = TokenizerBase.create(PropertiesSplitter.getInstance());
|
||||
private final Tokenizer<PropertyKeyImpl> myPropertyTokenizer = new PropertyKeyTokenizer();
|
||||
|
||||
@Override
|
||||
public @NotNull Tokenizer<?> getTokenizer(PsiElement element) {
|
||||
@@ -38,6 +39,17 @@ final class PropertiesSpellcheckingStrategy extends SpellcheckingStrategy implem
|
||||
return super.getTokenizer(element);
|
||||
}
|
||||
|
||||
private static class PropertyKeyTokenizer extends TokenizerBase<PropertyKeyImpl> {
|
||||
private PropertyKeyTokenizer() {
|
||||
super(PropertiesSplitter.getInstance());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consumeToken(@NotNull PropertyKeyImpl element, @NotNull TokenConsumer consumer, @NotNull Splitter splitter) {
|
||||
consumer.consumeToken(element, true, splitter);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PropertyValueTokenizer extends EscapeSequenceTokenizer<PropertyValueImpl> {
|
||||
@Override
|
||||
public void tokenize(@NotNull PropertyValueImpl element, @NotNull TokenConsumer consumer) {
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.intellij.lang.properties.rename
|
||||
|
||||
import com.intellij.lang.properties.psi.impl.PropertyKeyImpl
|
||||
import com.intellij.spellchecker.inspections.SpellCheckingInspection
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ProcessingContext
|
||||
|
||||
class PropertyRenameToTest : BasePlatformTestCase() {
|
||||
|
||||
fun `test suggestions does not contain space symbol`() {
|
||||
val validator = PropertyKeyRenameInputValidator()
|
||||
val context = ProcessingContext()
|
||||
myFixture.configureByText("a.properties", "hellow<caret>orld=value")
|
||||
val element = myFixture.file.findElementAt(myFixture.editor.caretModel.offset)!! as PropertyKeyImpl
|
||||
|
||||
assertTrue(validator.isInputValid("hello-world", element, context))
|
||||
assertTrue(validator.isInputValid("hello.world", element, context))
|
||||
assertTrue(validator.isInputValid("hello_world", element, context))
|
||||
assertFalse(validator.isInputValid("hello world", element, context))
|
||||
}
|
||||
|
||||
fun `test rename action is enabled and visible`() {
|
||||
myFixture.configureByText("a.properties", "<TYPO descr=\"Typo: In word 'helloworld'\">hellow<caret>orld</TYPO>=value")
|
||||
myFixture.enableInspections(SpellCheckingInspection())
|
||||
myFixture.checkHighlighting()
|
||||
myFixture.getAvailableIntention("Typo: Rename to…") ?: error("RenameTo intention is not available")
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
<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">
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// 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,16 +6,10 @@ 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
|
||||
@@ -26,19 +20,13 @@ 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")
|
||||
@@ -72,43 +60,14 @@ 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
|
||||
|
||||
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)
|
||||
@@ -120,23 +79,10 @@ internal class ChangeTo(typo: String, element: PsiElement, private val range: Te
|
||||
}
|
||||
|
||||
override fun getFileModifierForPreview(target: PsiFile): FileModifier {
|
||||
return ForPreview(index)
|
||||
return this
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ public class TokenizerBase<T extends PsiElement> extends Tokenizer<T> {
|
||||
return;
|
||||
}
|
||||
|
||||
consumer.consumeToken(element, mySplitter);
|
||||
consumeToken(element, consumer, mySplitter);
|
||||
}
|
||||
|
||||
public void consumeToken(@NotNull T element, @NotNull TokenConsumer consumer, @NotNull Splitter splitter) {
|
||||
consumer.consumeToken(element, splitter);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user