[grazie] IJPL-201886 Support language and text domain handling in Configure Rule action

Co-authored-by: Peter Gromov <peter.gromov@jetbrains.com>

Merge-request: IJ-MR-176906
Merged-by: Ilia Permiashkin <ilia.permiashkin@jetbrains.com>

GitOrigin-RevId: cc4ee0281c9ef6b9526c1aea1e46a9161a1ba699
This commit is contained in:
Ilia Permiashkin
2025-09-29 16:16:42 +00:00
committed by intellij-monorepo-bot
co-authored by Peter Gromov
parent d633ca2c95
commit 418f329ce1
13 changed files with 194 additions and 147 deletions
@@ -1,6 +1,7 @@
package com.intellij.grazie.grammar
import com.intellij.grazie.GrazieBundle
import com.intellij.grazie.detection.toLanguage
import com.intellij.grazie.ide.ui.components.utils.html
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.jlanguage.LangTool
@@ -17,7 +18,7 @@ import java.util.*
// ltRule used in ReSharper
class LanguageToolRule(
private val lang: Lang, val ltRule: org.languagetool.rules.Rule, private val similarLtRules: List<org.languagetool.rules.Rule> = emptyList(),
) : Rule(LangTool.globalIdPrefix(lang) + ltRule.id, ltRule.description, categories(ltRule, lang)) {
) : Rule(LangTool.globalIdPrefix(lang) + ltRule.id, lang.toLanguage(), ltRule.description, categories(ltRule, lang)) {
override fun isEnabledByDefault(domain: TextStyleDomain): Boolean = LangTool.isRuleEnabledByDefault(lang, ltRule.id, domain)
@@ -6,46 +6,44 @@ import com.intellij.codeInspection.IntentionAndQuickFixAction
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.ide.fus.GrazieFUSCounter
import com.intellij.grazie.ide.ui.components.dsl.msg
import com.intellij.grazie.ide.ui.grammar.GrazieConfigurable
import com.intellij.grazie.ide.ui.configurable.StyleConfigurable
import com.intellij.grazie.text.Rule
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.icons.AllIcons
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Iconable
import com.intellij.psi.PsiFile
import javax.swing.Icon
class GrazieRuleSettingsAction(private val ruleName: String, private val rule: Rule)
class GrazieRuleSettingsAction(private val rule: Rule, private val domain: TextStyleDomain)
: IntentionAndQuickFixAction(), Iconable, CustomizableIntentionAction, Comparable<IntentionAction>
{
override fun isShowSubmenu(): Boolean = false
override fun getIcon(flags: Int): Icon = AllIcons.Actions.Edit
override fun getName() = msg("grazie.grammar.quickfix.open.rule.text", ruleName)
override fun getName(): String = msg(when (domain) {
TextStyleDomain.Other -> "grazie.grammar.quickfix.open.rule.text"
TextStyleDomain.Commit -> "grazie.grammar.quickfix.open.rule.text.commit"
TextStyleDomain.CodeComment -> "grazie.grammar.quickfix.open.rule.text.comments"
TextStyleDomain.CodeDocumentation -> "grazie.grammar.quickfix.open.rule.text.documentation"
TextStyleDomain.AIPrompt -> "grazie.grammar.quickfix.open.rule.text.ai"
}, rule.presentableName)
override fun getFamilyName(): String = msg("grazie.grammar.quickfix.open.rule.family")
override fun startInWriteAction() = false
override fun startInWriteAction(): Boolean = false
override fun compareTo(other: IntentionAction): Int {
return if (other is GrazieAddExceptionQuickFix) 1 else 0
}
override fun applyFix(project: Project, psiFile: PsiFile?, editor: Editor?) {
val state1 = GrazieConfig.get()
val ok: Boolean
val navigatable = rule.editSettings()
if (navigatable != null && navigatable.canNavigate()) {
navigatable.navigate(true)
ok = true
} else {
ok = ShowSettingsUtil.getInstance().editConfigurable(project, GrazieConfigurable())
}
val result = if (!ok) "canceled" else analyzeStateChange(state1, GrazieConfig.get())
val state = GrazieConfig.get()
val ok = StyleConfigurable.focusSetting(rule.featuredSetting, rule, domain, rule.language, project)
val result = if (!ok) "canceled" else analyzeStateChange(state, GrazieConfig.get())
GrazieFUSCounter.settingsUpdated("rule.settings:$result", rule, project)
}
@@ -11,14 +11,12 @@ import ai.grazie.rules.toolkit.LanguageToolkit
import com.intellij.grazie.GrazieBundle
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.detection.toLanguage
import com.intellij.grazie.ide.ui.components.dsl.msg
import com.intellij.grazie.ide.ui.configurable.StyleConfigurable.Companion.ruleEngineLanguages
import com.intellij.grazie.ide.ui.grammar.tabs.rules.component.GrazieDescriptionComponent
import com.intellij.grazie.ide.ui.grammar.tabs.rules.component.GrazieTreeComponent
import com.intellij.grazie.rule.RuleIdeClient
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.grazie.utils.getAffectedGlobalRules
import com.intellij.grazie.utils.getOtherDomainStyles
import com.intellij.grazie.utils.getTextDomain
import com.intellij.grazie.utils.*
import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.Disposable
@@ -31,7 +29,6 @@ import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.getParentOfType
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.pom.Navigatable
import com.intellij.psi.codeStyle.NameUtil
import com.intellij.ui.*
import com.intellij.ui.components.JBLabel
@@ -55,7 +52,6 @@ import javax.swing.event.DocumentEvent
import javax.swing.event.HyperlinkEvent
class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.settings.grammar.tabs.rules"), null), Disposable, Configurable.NoScroll {
private var focusedControl: JComponent? = null
private val settings: Settings = Settings()
private val langComboModel = CollectionComboBoxModel(ArrayList<Language>())
private lateinit var langCombo: ComboBox<Language>
@@ -72,7 +68,7 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
private val treeWrapper by lazy {
JBSplitter(false, 0.45f).apply {
firstComponent = createScrollTreeComponent()
firstComponent = createScrollTreeComponent(textStyle, Language.ENGLISH)
secondComponent = settings.getTreeSettings(textStyle, Language.ENGLISH).description.component
}
}
@@ -118,7 +114,6 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
label(GrazieBundle.message("grazie.settings.writing.style.domain"))
domainComboBox = domainComboBox()
.whenItemSelectedFromUi { domainId ->
styleRowVisibleUpdater.invoke(domainId == TextStyleDomain.Other)
val textStyle = if (domainId == TextStyleDomain.Other) GrazieConfig.get().getTextStyle() else domainId.textStyle
selectTextStyle(textStyle, langComboModel.selected!!)
}
@@ -151,8 +146,7 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
langCombo = comboBox(langComboModel, SimpleListCellRenderer.create { label, lang, _ -> label.text = lang.nativeName })
.widthGroup("TopCombo")
.whenItemSelectedFromUi { language ->
settings.addTextStyle(textStyle, language, filterComponent)
selectLanguage(textStyle, language)
selectTextStyle(textStyle, language)
separator.text = GrazieBundle.message(if (language in ruleEngineLanguages) "grazie.settings.style.rules.other" else "grazie.settings.style.rules.all")
if (filterComponent.text.isNotBlank()) settings.updateFilter(textStyle, language, filterComponent.text)
settings.getTreeSettings(textStyle, language).description.listener(language)
@@ -190,11 +184,11 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
override fun createPanel(): DialogPanel = component
override fun isModified(): Boolean = super<BoundConfigurable>.isModified || settings.isModified()
override fun isModified(): Boolean = super<BoundConfigurable>.isModified || settings.isModified(GrazieConfig.get())
override fun apply() {
super.apply()
settings.apply()
settings.apply(GrazieConfig.get())
}
override fun reset() {
@@ -207,7 +201,12 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
}
private fun selectTextStyle(textStyle: TextStyle, language: Language) {
val domain = textStyle.getTextDomain()
if (domain != TextStyleDomain.Other) domainComboBox.selectedItem = domain else styleProfileCombo.selectedItem = textStyle
styleRowVisibleUpdater.invoke(domain == TextStyleDomain.Other)
settings.addTextStyle(textStyle, language, filterComponent)
langCombo.selectedItem = language
repaintSettings(textStyle, language)
}
@@ -226,7 +225,8 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
settingWrapper.maximumSize = Dimension(Int.MAX_VALUE, settingWrapper.preferredSize.height)
settingWrapper.repaint()
}
treeWrapper.firstComponent = createScrollTreeComponent()
treeWrapper.removeAll()
treeWrapper.firstComponent = createScrollTreeComponent(textStyle, language)
treeWrapper.secondComponent = settings.getTreeSettings(textStyle, language).description.component
treeWrapper.repaint()
}
@@ -248,15 +248,11 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
return true
}
override fun getDisplayName(): @NlsContexts.ConfigurableName String = ""
override fun getDisplayName(): @NlsContexts.ConfigurableName String = msg("grazie.settings.page.name")
override fun getPreferredFocusedComponent(): JComponent? {
return focusedControl ?: super.getPreferredFocusedComponent()
}
private fun createScrollTreeComponent(): JScrollPane {
private fun createScrollTreeComponent(textStyle: TextStyle, language: Language): JScrollPane {
return ScrollPaneFactory.createScrollPane(
settings.getTreeSettings(textStyle, Language.ENGLISH).tree,
settings.getTreeSettings(textStyle, language).tree,
VERTICAL_SCROLLBAR_AS_NEEDED,
HORIZONTAL_SCROLLBAR_AS_NEEDED
)
@@ -268,37 +264,61 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
val ruleEngineLanguages: List<Language> = listOf(Language.ENGLISH, Language.GERMAN, Language.RUSSIAN, Language.UKRAINIAN)
@JvmStatic
fun featuredSettings(toolkit: LanguageToolkit): List<Setting> = toolkit.getSettings(RuleIdeClient.INSTANCE).flatMap { it.settings }
fun focusSetting(setting: Setting, domain: TextStyleDomain, language: Language, project: Project): Boolean {
return focusSetting(setting, null, domain, language, project)
}
@JvmStatic
fun focusSetting(setting: Setting, contextProject: Project): Navigatable {
return object : Navigatable {
override fun navigate(requestFocus: Boolean) {
ShowSettingsUtil.getInstance().showSettingsDialog(contextProject, StyleConfigurable::class.java) { conf ->
conf.createComponent()
val style = getTextStyle(GrazieConfig.get().styleProfile ?: TextStyle.Unspecified.id)
val featuredSettings = conf.settings.featuredSettings[style.id]!!
for ((lang, data) in featuredSettings) {
val settingComponent = data.component.findParentSettingComponent(setting)
val paramComponent = data.component.findOwnSettingComponent(setting)
if (paramComponent != null && settingComponent != null) {
conf.langComboModel.add(lang)
conf.selectLanguage(style, lang)
UiNotifyConnector.doWhenFirstShown(data.component) {
SwingUtilities.invokeLater {
val scrollPane = settingComponent.getParentOfType<JBScrollPane>()!!
settingComponent.scrollRectToVisible(Rectangle(settingComponent.width, scrollPane.height))
IdeFocusManager.getInstance(contextProject).requestFocus(paramComponent, true)
}
}
break
}
}
fun focusSetting(setting: Setting?, rule: com.intellij.grazie.text.Rule?, domain: TextStyleDomain, language: Language, project: Project): Boolean {
require(setting != null || rule != null) { "Setting and Rule can't be null" }
val configurable = StyleConfigurable().apply {
createComponent()
val style = if (domain == TextStyleDomain.Other) GrazieConfig.get().getTextStyle() else domain.textStyle
selectTextStyle(style, language)
if (setting != null && featuredSettings(language).contains(setting)) focusFeaturedSetting(this, setting, style, language, project)
else if (rule != null) focusTreeSetting(this, rule, style, language, project)
}
return ShowSettingsUtil.getInstance().editConfigurable(project, configurable)
}
}
private fun focusFeaturedSetting(styleConfigurable: StyleConfigurable, setting: Setting, style: TextStyle, language: Language, project: Project) {
val data = settings.getFeaturedSettings(style, language)
if (data == null) return
styleConfigurable.apply {
val settingComponent = data.component.findParentSettingComponent(setting)
val paramComponent = data.component.findOwnSettingComponent(setting)
if (paramComponent != null && settingComponent != null) {
UiNotifyConnector.doWhenFirstShown(data.component) {
SwingUtilities.invokeLater {
val scrollPane = settingComponent.getParentOfType<JBScrollPane>()!!
settingComponent.scrollRectToVisible(Rectangle(settingComponent.width, scrollPane.height))
IdeFocusManager.getInstance(project).requestFocus(paramComponent, true)
}
}
}
}
}
override fun canNavigate() = true
override fun canNavigateToSource() = false
private fun focusTreeSetting(styleConfigurable: StyleConfigurable, rule: com.intellij.grazie.text.Rule, style: TextStyle, language: Language, project: Project) {
val data = settings.getTreeSettings(style, language)
styleConfigurable.apply {
data.tree.focusRule(rule)
UiNotifyConnector.doWhenFirstShown(data.tree) {
SwingUtilities.invokeLater {
val scroll = data.tree.getParentOfType<JBScrollPane>()
val dataTreeScroll = scroll?.getParentOfType<JBScrollPane>()
val componentScroll = dataTreeScroll?.getParentOfType<JBScrollPane>()
if (dataTreeScroll == null || componentScroll == null) return@invokeLater
val destination = SwingUtilities.convertRectangle(
scroll,
Rectangle(0, 0, scroll.width, scroll.height),
dataTreeScroll.viewport.view
)
componentScroll.scrollRectToVisible(destination)
IdeFocusManager.getInstance(project).requestFocus(data.tree, true)
}
}
}
}
@@ -306,19 +326,19 @@ class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.setting
data class Settings(
val featuredSettings: MutableMap<String, MutableMap<Language, FeaturedSettings>> = HashMap(),
private val treeSettings: MutableMap<String, MutableMap<Language, TreeSettings>> = HashMap(),
val treeSettings: MutableMap<String, MutableMap<Language, TreeSettings>> = HashMap(),
) {
fun getFeaturedSettings(textStyle: TextStyle, language: Language): FeaturedSettings? = featuredSettings[textStyle.id]?.get(language)
fun getTreeSettings(textStyle: TextStyle, language: Language): TreeSettings = treeSettings[textStyle.id]!![language]!!
fun isModified(): Boolean =
featuredSettings.values.any { isModifiedFeaturedSettings(it) } ||
treeSettings.values.any { isModifiedTreeSettings(it) }
fun isModified(state: GrazieConfig.State): Boolean =
featuredSettings.values.any { areModifiedFeaturedSettings(it) } ||
treeSettings.values.any { areModifiedTreeSettings(it, state) }
fun reset(state: GrazieConfig.State) {
featuredSettings.forEach { (domain, featuredSettings) ->
if (isModifiedFeaturedSettings(featuredSettings)) {
featuredSettings.forEach { (language, settings) ->
if (areModifiedFeaturedSettings(featuredSettings)) {
featuredSettings.filter { areModifiedFeaturedSettings(it.value) }.forEach { (language, settings) ->
val textStyle = getTextStyle(domain)
settings.component.loadState(getSettingsState(language, textStyle), textStyle)
settings.resetState = settings.component.state
@@ -326,57 +346,63 @@ data class Settings(
}
}
treeSettings.forEach { (_, treesSettings) ->
if (isModifiedTreeSettings(treesSettings)) {
treesSettings.forEach { (_, settings) -> settings.tree.reset(state) }
if (areModifiedTreeSettings(treesSettings, state)) {
treesSettings.filter { areModifiedTreeSettings(it.value, state) }.forEach { (_, settings) -> settings.tree.reset(state) }
}
}
}
fun apply() {
fun apply(originalState: GrazieConfig.State) {
treeSettings
.filter { (domainId, treeSettingsMap) -> isModifiedFeaturedSettings(featuredSettings[domainId]!!) || isModifiedTreeSettings(treeSettingsMap) }
.filter { (domainId, treeSettingsMap) -> areModifiedFeaturedSettings(featuredSettings[domainId]!!) || areModifiedTreeSettings(treeSettingsMap, originalState) }
.forEach { (domainId, treeSettingsMap) ->
val domain = getTextStyle(domainId).getTextDomain()
val userEnabledRules = HashSet<String>()
val userDisabledRules = HashSet<String>()
val parameters = HashMap<Language, Map<String, String>>()
featuredSettings[domainId]?.forEach { (language, settings) ->
val prefix = Rule.globalIdPrefix(language)
val settingsState = settings.component.state
settings.resetState = settingsState
treeSettingsMap.filter { areModifiedTreeSettings(it.value, originalState) }.forEach { (language, treeSettings) ->
val userEnabledRulesPerLanguage = HashSet<String>()
val userDisabledRulesPerLanguage = HashSet<String>()
for (id in settingsState.enabledRules) {
userEnabledRules.add(prefix + id)
userDisabledRules.remove(prefix + id)
featuredSettings[domainId]?.filter { areModifiedFeaturedSettings(it.value) }?.get(language)?.let { featuredSettings ->
val prefix = Rule.globalIdPrefix(language)
val settingsState = featuredSettings.component.state
featuredSettings.resetState = settingsState
for (id in settingsState.enabledRules) {
userEnabledRulesPerLanguage.add(prefix + id)
userDisabledRulesPerLanguage.remove(prefix + id)
}
for (id in settingsState.disabledRules) {
userEnabledRulesPerLanguage.remove(prefix + id)
userDisabledRulesPerLanguage.add(prefix + id)
}
parameters[language] = settingsState.paramValues
}
for (id in settingsState.disabledRules) {
userEnabledRules.remove(prefix + id)
userDisabledRules.add(prefix + id)
val updatedState = treeSettings.tree.apply(originalState)
val affectedGlobalRules = getAffectedGlobalRules(language)
updatedState.getUserChangedRules(domain).let { (enabledRules, disabledRules) ->
userEnabledRulesPerLanguage.addAll(enabledRules - affectedGlobalRules)
userDisabledRulesPerLanguage.addAll(disabledRules - affectedGlobalRules)
}
parameters[language] = settingsState.paramValues
userEnabledRules.addAll(userEnabledRulesPerLanguage)
userDisabledRules.addAll(userDisabledRulesPerLanguage)
}
if (domain == TextStyleDomain.Other) GrazieConfig.update { it.copy(parameters = parameters) }
else GrazieConfig.update { it.copy(parametersPerDomain = mapOf(domain to parameters)) }
treeSettingsMap.forEach {
val updatedState = it.value.tree.apply(GrazieConfig.get())
val affectedGlobalRules = getAffectedGlobalRules(it.key)
updatedState.getUserChangedRules(domain).let { (enabledRules, disabledRules) ->
userEnabledRules.addAll(enabledRules - affectedGlobalRules)
userDisabledRules.addAll(disabledRules - affectedGlobalRules)
}
}
GrazieConfig.update { it.updateUserRules(domain, userEnabledRules, userDisabledRules) }
treeSettingsMap.forEach { it.value.tree.reset(GrazieConfig.get()) }
}
}
fun addTextStyle(textStyle: TextStyle, language: Language, filterComponent: SearchTextField) {
if (textStyle.id in featuredSettings && textStyle.id in treeSettings) return
featuredSettings[textStyle.id] = HashMap()
treeSettings[textStyle.id] = HashMap()
val settings = treeSettings[textStyle.id]
if (settings != null && language in settings) return
if (featuredSettings[textStyle.id] == null) featuredSettings[textStyle.id] = HashMap()
if (treeSettings[textStyle.id] == null) treeSettings[textStyle.id] = HashMap()
addLanguage(textStyle, language, filterComponent)
}
@@ -451,12 +477,20 @@ data class Settings(
treeSettings.clear()
}
private fun isModifiedFeaturedSettings(featuredSettings: MutableMap<Language, FeaturedSettings>): Boolean {
return featuredSettings.any { it.value.component.state != it.value.resetState }
private fun areModifiedFeaturedSettings(featuredSettings: MutableMap<Language, FeaturedSettings>): Boolean {
return featuredSettings.any { areModifiedFeaturedSettings(it.value) }
}
private fun isModifiedTreeSettings(treeSettings: MutableMap<Language, TreeSettings>): Boolean {
return treeSettings.any { it.value.tree.isModified(GrazieConfig.get()) }
private fun areModifiedTreeSettings(treeSettings: MutableMap<Language, TreeSettings>, state: GrazieConfig.State): Boolean {
return treeSettings.any { areModifiedTreeSettings(it.value, state) }
}
private fun areModifiedFeaturedSettings(featuredSettings: FeaturedSettings): Boolean {
return featuredSettings.component.state != featuredSettings.resetState
}
private fun areModifiedTreeSettings(treeSettings: TreeSettings, state: GrazieConfig.State): Boolean {
return treeSettings.tree.isModified(state)
}
}
@@ -105,6 +105,13 @@ class GrazieTreeComponent(
fun getCurrentFilterString(): String? = filter.text
fun focusRule(rule: Rule) {
val ruleNode = (model.root as GrazieRulesTreeNode).findRuleNode(rule.globalId)
if (ruleNode != null) {
TreeUtil.selectNode(this, ruleNode)
}
}
fun resetTreeModel(rules: List<Rule>) {
val root = GrazieRulesTreeNode(domain)
val model = model as DefaultTreeModel
@@ -1,14 +1,17 @@
package com.intellij.grazie.style;
import ai.grazie.nlp.langs.Language;
import ai.grazie.rules.tree.Parameter;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.grazie.ide.ui.configurable.StyleConfigurable;
import com.intellij.grazie.utils.TextStyleDomain;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
public record ConfigureSuggestedParameter(Parameter parameter, String text) implements LocalQuickFix {
public record ConfigureSuggestedParameter(Parameter parameter, TextStyleDomain domain, Language language, String text)
implements LocalQuickFix {
public ConfigureSuggestedParameter {
assert !parameter.id().equals(Parameter.LANGUAGE_VARIANT);
@@ -26,7 +29,7 @@ public record ConfigureSuggestedParameter(Parameter parameter, String text) impl
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
StyleConfigurable.focusSetting(parameter, project).navigate(true);
StyleConfigurable.focusSetting(parameter, domain, language, project);
//todo FUS?
}
}
@@ -20,6 +20,7 @@ import com.intellij.grazie.ide.inspection.grammar.quickfix.GrazieCustomFixWrappe
import com.intellij.grazie.ide.inspection.grammar.quickfix.GrazieReplaceTypoQuickFix
import com.intellij.grazie.ide.inspection.grammar.quickfix.GrazieRuleSettingsAction
import com.intellij.grazie.ide.language.LanguageGrammarChecking
import com.intellij.grazie.utils.getTextDomain
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.ProgressManager
@@ -233,7 +234,7 @@ class CheckerRunner(val text: TextContent) {
super.applyFix(project, psiFile, editor)
}
})
result.add(GrazieRuleSettingsAction(problem.rule.presentableName, problem.rule))
result.add(GrazieRuleSettingsAction(problem.rule, problem.text.getTextDomain()))
return result.toTypedArray()
}
@@ -1,9 +1,10 @@
package com.intellij.grazie.text;
import ai.grazie.nlp.langs.Language;
import ai.grazie.rules.settings.Setting;
import com.intellij.grazie.GrazieConfig;
import com.intellij.grazie.utils.TextStyleDomain;
import com.intellij.grazie.utils.TextUtilsKt;
import com.intellij.pom.Navigatable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -14,15 +15,16 @@ import java.util.Objects;
/** A representation of rules from various {@link TextChecker}s. */
public abstract class Rule {
private final String globalId;
private final Language language;
private final String presentableName;
private final List<String> categories;
/**
* Create a rule in a single category
* @see #Rule(String, String, List)
* @see #Rule(String, Language, String, List)
*/
public Rule(String globalId, String presentableName, String category) {
this(globalId, presentableName, List.of(category));
public Rule(String globalId, Language language, String presentableName, String category) {
this(globalId, language, presentableName, List.of(category));
}
/**
@@ -34,8 +36,9 @@ public abstract class Rule {
* @param categories a non-empty list of the presentable names of the rule's categories.
* It's used to group the rules in the settings, starting from the language node.
*/
public Rule(String globalId, String presentableName, List<String> categories) {
public Rule(String globalId, Language language, String presentableName, List<String> categories) {
this.globalId = globalId;
this.language = language;
this.presentableName = presentableName;
this.categories = List.copyOf(categories);
if (categories.isEmpty()) {
@@ -54,6 +57,13 @@ public abstract class Rule {
return presentableName;
}
/**
* @return the language that the rule is applicable to
*/
public Language getLanguage() {
return language;
}
/**
* @return a text/HTML description of what the rule does, to be displayed in settings
*/
@@ -100,9 +110,9 @@ public abstract class Rule {
}
/**
* @return an optional navigatable to open from "Rule X settings" quick fix.
* @return an optional featured rule setting to open from "Rule X settings" quick fix.
*/
public @Nullable Navigatable editSettings() {
public @Nullable Setting getFeaturedSetting() {
return null;
}
@@ -11,6 +11,7 @@ import ai.grazie.rules.document.Delimiter;
import ai.grazie.rules.document.DocumentRule;
import ai.grazie.rules.document.DocumentSentence;
import ai.grazie.rules.settings.RuleSetting;
import ai.grazie.rules.settings.Setting;
import ai.grazie.rules.settings.TextStyle;
import ai.grazie.rules.toolkit.LanguageToolkit;
import ai.grazie.rules.tree.ActionSuggestion;
@@ -35,15 +36,12 @@ import com.intellij.grazie.text.TextContent.TextDomain;
import com.intellij.grazie.utils.HighlightingUtil;
import com.intellij.grazie.utils.Text;
import com.intellij.grazie.utils.TextStyleDomain;
import com.intellij.grazie.utils.TextUtilsKt;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vcs.ui.CommitMessage;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiPlainTextFile;
import com.intellij.psi.util.CachedValueProvider;
@@ -134,7 +132,7 @@ public final class TreeRuleChecker {
List<String> categories = rule.isStyleLike()
? List.of(styleCategories.get(langCode))
: List.of(category);
return new Rule(id, rule.displayName, categories.getFirst()) {
return new Rule(id, rule.language(), rule.displayName, categories.getFirst()) {
@Override
public List<String> getCategories() {
@@ -170,14 +168,9 @@ public final class TreeRuleChecker {
return rule.isRuleEnabledByDefault(GrazieConfig.Companion.get().getTextStyle(domain), RuleIdeClient.INSTANCE);
}
@SuppressWarnings("SuspiciousMethodCalls")//false negative in Qodana
@Override
public Navigatable editSettings() {
RuleSetting setting = new RuleSetting(rule);
LanguageToolkit toolkit = LanguageToolkit.forLanguage(rule.language());
return StyleConfigurable.featuredSettings(toolkit).contains(setting)
? StyleConfigurable.focusSetting(setting, null)
: null;
public Setting getFeaturedSetting() {
return new RuleSetting(rule);
}
@Override
@@ -540,6 +533,7 @@ public final class TreeRuleChecker {
public static class TreeProblem extends GrazieProblem {
public final RuleMatch match;
private final List<LocalQuickFix> customFixes;
private final TextStyleDomain domain;
TreeProblem(Problem problem, RuleMatch match, TextContent text) {
this(problem, toGrazieRule(match.rule()), text, match, List.of());
@@ -551,6 +545,7 @@ public final class TreeRuleChecker {
super(problem, ideaRule, text);
this.match = match;
this.customFixes = customFixes;
this.domain = getTextDomain(text);
}
@Override
@@ -567,7 +562,7 @@ public final class TreeRuleChecker {
if (parameter.id().equals(Parameter.LANGUAGE_VARIANT)) {
return ChangeLanguageVariant.create(match.rule().language(), Objects.requireNonNull(suggestedValue), quickFixText);
}
return new ConfigureSuggestedParameter(parameter, quickFixText);
return new ConfigureSuggestedParameter(parameter, domain, match.rule().language(), quickFixText);
}
if (sug == ActionSuggestion.REPHRASE) {
return new RephraseAction();
@@ -612,24 +607,6 @@ public final class TreeRuleChecker {
public boolean shouldSuppressInCodeLikeFragments() {
return match.rule().shouldSuppressInCodeLikeFragments();
}
private record MySuggestion(ProblemFix fix, TextContent text) implements TextProblem.Suggestion {
@Override
public List<StringOperation> getChanges() {
return ContainerUtil.map(fix.getChanges(), r -> StringOperation.replace(ijRange(r), r.getText()));
}
@Override
public String getPresentableText() {
return getQuickFixText(fix);
}
@Override
public @Nullable String getBatchId() {
return fix.getBatchId();
}
}
}
public static class DocProblemFilter extends ProblemFilter {
@@ -4,10 +4,11 @@ import ai.grazie.gec.model.problem.ProblemHighlighting
import ai.grazie.nlp.langs.Language
import ai.grazie.rules.Rule
import ai.grazie.rules.settings.RuleSetting
import ai.grazie.rules.settings.Setting
import ai.grazie.rules.toolkit.LanguageToolkit
import com.intellij.grazie.detection.LangDetector
import com.intellij.grazie.ide.ui.configurable.StyleConfigurable.Companion.featuredSettings
import com.intellij.grazie.ide.ui.configurable.StyleConfigurable.Companion.ruleEngineLanguages
import com.intellij.grazie.rule.RuleIdeClient
import com.intellij.openapi.util.TextRange
import com.intellij.util.containers.ContainerUtil.createConcurrentSoftValueMap
import ai.grazie.text.TextRange as GrazieTextRange
@@ -17,15 +18,24 @@ private val affectedGlobalRules = createConcurrentSoftValueMap<Language, Set<Str
fun getAffectedGlobalRules(language: Language): Set<String> {
if (language !in ruleEngineLanguages) return emptySet()
return affectedGlobalRules.computeIfAbsent(language) {
val toolkit = LanguageToolkit.forLanguage(language)
val prefix = Rule.globalIdPrefix(language)
featuredSettings(toolkit)
featuredSettings(language)
.filterIsInstance<RuleSetting>()
.map { prefix + it.rule.id }
.toHashSet()
}
}
fun featuredSettings(language: Language): List<Setting> =
LanguageToolkit.forLanguage(language).getSettings(RuleIdeClient.INSTANCE)
.flatMap { it.settings() }
.flatMap { setting ->
when (setting) {
is RuleSetting -> listOf(setting) + setting.subSettings
else -> listOf(setting)
}
}
fun getLanguageIfAvailable(text: String): Language? {
return LangDetector.getLanguage(text)?.takeIf { HighlightingUtil.findInstalledLang(it) != null }
}
@@ -31,6 +31,10 @@ grazie.grammar.inspection.grammar.text=Grammar
grazie.grammar.quickfix.open.rule.family=Configure rule
grazie.grammar.quickfix.open.rule.text=Configure rule ''{0}''\u2026
grazie.grammar.quickfix.open.rule.text.commit=Configure rule ''{0}'' in commit messages\u2026
grazie.grammar.quickfix.open.rule.text.documentation=Configure rule ''{0}'' in documentation\u2026
grazie.grammar.quickfix.open.rule.text.comments=Configure rule ''{0}'' in comments\u2026
grazie.grammar.quickfix.open.rule.text.ai=Configure rule ''{0}'' in AI prompts\u2026
grazie.grammar.quickfix.ignore.family=Add exception
grazie.grammar.quickfix.ignore.text.no.context=Ignore ''{0}''
@@ -1,5 +1,6 @@
package com.intellij.grazie.ide.language
import ai.grazie.nlp.langs.Language
import com.intellij.grazie.text.*
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.extensions.ExtensionPointName
@@ -110,7 +111,7 @@ class CheckerRunnerTest: BasePlatformTestCase() {
}
private fun createFakeProblem(text: TextContent, range: TextRange): TextProblem {
val rule = object: Rule("fake.global.id", "Fake rule", "Fake category") {
val rule = object : Rule("fake.global.id", Language.UNKNOWN, "Fake rule", "Fake category") {
override fun getDescription(): String {
return "Fake rule description"
}
@@ -25,7 +25,7 @@ class CommitSupportTest : GrazieTestBase() {
myFixture.findSingleIntention("Wrong article")
myFixture.findSingleIntention("an")
myFixture.findSingleIntention("Ignore 'a error'")
myFixture.findSingleIntention("Configure rule 'Use of 'a' vs. 'an''…")
myFixture.findSingleIntention("Configure rule 'Use of 'a' vs. 'an'' in commit messages…")
}
}
@@ -1,5 +1,6 @@
package com.intellij.grazie.ide.language
import ai.grazie.nlp.langs.Language
import com.intellij.codeInsight.CodeInsightBundle
import com.intellij.codeInsight.daemon.impl.actions.IntentionActionWithFixAllOption
import com.intellij.codeInsight.intention.CustomizableIntentionAction
@@ -180,7 +181,7 @@ class ReportingTest : BasePlatformTestCase() {
range: TextRange,
suggestions: List<TextProblem.Suggestion>,
customFixes: List<LocalQuickFix>): TextProblem {
val rule = object : Rule("something.something", "something", "something") {
val rule = object : Rule("something.something", Language.UNKNOWN, "something", "something") {
override fun getDescription() = "something"
}
return object : TextProblem(rule, text, range) {