[grazie] IJPL-201886 Add per-domain writing style settings to Grammar and Style | Rules

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

GitOrigin-RevId: beea0ca1c4e93543b3126384c96b42d068516ac8
This commit is contained in:
Ilia Permiashkin
2025-09-24 17:15:14 +00:00
committed by intellij-monorepo-bot
parent cd5ef01926
commit 32bc57f455
30 changed files with 840 additions and 491 deletions
@@ -41,13 +41,6 @@
</content>
<extensions defaultExtensionNs="com.intellij">
<applicationConfigurable
parentId="proofread"
instance="com.intellij.grazie.ide.ui.configurable.StyleConfigurable"
key="grazie.settings.style.configurable.name"
bundle="messages.GrazieBundle"
id="reference.settings.grazie"/>
<projectConfigurable parentId="proofread" instance="com.intellij.grazie.spellcheck.settings.SpellCheckerSettingsManager"
id="reference.settings.ide.settings.spelling"
key="spelling"
@@ -18,6 +18,7 @@ import com.intellij.grazie.jlanguage.LangTool
import com.intellij.grazie.remote.GrazieRemote.isAvailableLocally
import com.intellij.grazie.rule.RuleIdeClient
import com.intellij.grazie.text.Rule
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.*
import com.intellij.openapi.project.ProjectManager
@@ -51,8 +52,8 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
//Since commit cc47dd17
NEW_UI;
override fun next() = values().getOrNull(ordinal + 1)
override fun toString() = ordinal.toString()
override fun next(): Version? = entries.getOrNull(ordinal + 1)
override fun toString(): String = ordinal.toString()
companion object {
val CURRENT = NEW_UI
@@ -76,6 +77,8 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
@Deprecated("Moved to checkingContext in version 2") @ApiStatus.ScheduledForRemoval @Property val enabledCommitIntegration: Boolean = false,
@Property val userDisabledRules: Set<String> = HashSet(),
@Property val userEnabledRules: Set<String> = HashSet(),
@Property val domainDisabledRules: Map<TextStyleDomain, Set<String>> = TreeMap(),
@Property val domainEnabledRules: Map<TextStyleDomain, Set<String>> = TreeMap(),
//Formerly suppressionContext -- name changed due to compatibility issues
@Property val suppressingContext: SuppressingContext = SuppressingContext(),
@Property val detectionContext: DetectionContext.State = DetectionContext.State(),
@@ -84,6 +87,7 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
//Ex. Grazie pro properties
@Property val styleProfile: String? = TextStyle.Unspecified.id,
@Property val parameters: Map<Language, Map<String, String>> = TreeMap(),
@Property val parametersPerDomain: Map<TextStyleDomain, Map<Language, Map<String, String>>> = TreeMap(),
@Property val useOxfordSpelling: Boolean = false,
@Property val autoFix: Boolean = false,
) : VersionedState<Version, State> {
@@ -104,7 +108,7 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
val missedLanguages: Set<Lang>
get() = enabledLanguages.asSequence().filter { isMissingLanguage(it) }.toCollection(CollectionFactory.createSmallMemoryFootprintLinkedSet())
override fun increment() = copy(version = version.next() ?: error("Attempt to increment latest version $version"))
override fun increment(): State = copy(version = version.next() ?: error("Attempt to increment latest version $version"))
fun hasMissedLanguages(): Boolean {
return enabledLanguages.any { isMissingLanguage(it) }
@@ -114,32 +118,102 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
return !isAvailableLocally(lang) && lang.jLanguage == null
}
fun withAutoFix(autoFix: Boolean): State = copy(autoFix = autoFix)
fun withOxfordSpelling(useOxford: Boolean): State = copy(useOxfordSpelling = useOxford)
fun withParameter(language: Language, parameter: Parameter, value: String?): State {
val newLangParams = TreeMap(parameters[language] ?: emptyMap())
if (value != null) {
newLangParams[parameter.id()] = value
}
else {
newLangParams.remove(parameter.id())
}
val newParams = TreeMap(parameters)
if (newLangParams.isEmpty()) {
newParams.remove(language)
}
else {
newParams[language] = newLangParams
}
return copy(parameters = newParams)
fun isRuleEnabled(ruleId: String, domain: TextStyleDomain): Boolean {
return ruleId in (if (domain == TextStyleDomain.Other) userEnabledRules else domainEnabledRules[domain] ?: emptySet())
}
val textStyle: TextStyle
get() = TextStyle.styles(RuleIdeClient.INSTANCE).find { it.id == styleProfile } ?: TextStyle.Unspecified
fun isRuleDisabled(ruleId: String, domain: TextStyleDomain): Boolean {
return ruleId in (if (domain == TextStyleDomain.Other) userDisabledRules else domainDisabledRules[domain] ?: emptySet())
}
fun paramValue(language: Language, parameter: Parameter): String? {
return parameters[language]?.get(parameter.id())
fun withAutoFix(autoFix: Boolean): State = copy(autoFix = autoFix)
fun withOxfordSpelling(useOxford: Boolean): State = copy(useOxfordSpelling = useOxford)
fun withParameter(domain: TextStyleDomain, language: Language, parameter: Parameter, value: String?): State {
if (domain == TextStyleDomain.Other) {
val newLangParams = TreeMap(parameters[language] ?: emptyMap())
if (value != null) newLangParams[parameter.id()] = value else newLangParams.remove(parameter.id())
val newParams = TreeMap(parameters)
if (newLangParams.isEmpty()) newParams.remove(language) else newParams[language] = newLangParams
return copy(parameters = newParams)
}
val newParamsPerDomain = TreeMap(parametersPerDomain)
val langsInDomain = TreeMap(newParamsPerDomain[domain] ?: emptyMap())
val newLangParams = TreeMap(langsInDomain[language] ?: emptyMap())
if (value != null) newLangParams[parameter.id()] = value else newLangParams.remove(parameter.id())
if (newLangParams.isEmpty()) {
langsInDomain.remove(language)
} else {
langsInDomain[language] = newLangParams
}
if (langsInDomain.isEmpty()) {
newParamsPerDomain.remove(domain)
} else {
newParamsPerDomain[domain] = langsInDomain
}
return copy(parametersPerDomain = newParamsPerDomain)
}
fun getUserChangedRules(domain: TextStyleDomain): UserChangedRules {
val userEnabledRules = HashSet<String>()
val userDisabledRules = HashSet<String>()
val isOtherDomain = domain == TextStyleDomain.Other
if (isOtherDomain) {
userEnabledRules.addAll(this.userEnabledRules)
userDisabledRules.addAll(this.userDisabledRules)
}
else {
userEnabledRules.addAll(getDomainEnabledRules(domain))
userDisabledRules.addAll(getDomainDisabledRules(domain))
}
return UserChangedRules(userEnabledRules, userDisabledRules)
}
fun updateUserRules(domain: TextStyleDomain, userEnabledRules: Set<String>, userDisabledRules: Set<String>): State {
val userRules = getUserChangedRules(domain)
if (userEnabledRules == userRules.enabled && userDisabledRules == userRules.disabled) {
return this
}
return if (domain == TextStyleDomain.Other) this.copy(userEnabledRules = userEnabledRules, userDisabledRules = userDisabledRules)
else this.withDomainEnabledRules(domain, userEnabledRules).withDomainDisabledRules(domain, userDisabledRules)
}
private fun getDomainEnabledRules(domain: TextStyleDomain) = domainEnabledRules[domain] ?: emptySet()
private fun getDomainDisabledRules(domain: TextStyleDomain) = domainDisabledRules[domain] ?: emptySet()
fun withDomainEnabledRules(domain: TextStyleDomain, rules: Set<String>): State {
val newRules = TreeMap(domainEnabledRules)
newRules[domain] = rules
return copy(domainEnabledRules = newRules)
}
fun withDomainDisabledRules(domain: TextStyleDomain, rules: Set<String>): State {
val newRules = TreeMap(domainDisabledRules)
newRules[domain] = rules
return copy(domainDisabledRules = newRules)
}
fun paramValue(domain: TextStyleDomain, language: Language, parameter: Parameter): String? {
if (domain == TextStyleDomain.Other) {
return parameters[language]?.get(parameter.id())
}
return parametersPerDomain[domain]?.get(language)?.get(parameter.id())
}
@JvmOverloads
fun getTextStyle(domain: TextStyleDomain? = null): TextStyle {
val styleProfileId = getTextStyleId(domain) ?: return TextStyle.Unspecified
return TextStyle.styles(RuleIdeClient.INSTANCE).find { it.id == styleProfileId } ?: TextStyle.Unspecified
}
private fun getTextStyleId(domain: TextStyleDomain? = null): String? {
return if (domain == null || domain == TextStyleDomain.Other) styleProfile else domain.name
}
enum class Processing {
@@ -155,7 +229,7 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
@VisibleForTesting
fun migrateLTRuleIds(state: State): State {
val ltRules: List<Rule> by lazy {
state.enabledLanguages.filter { it.jLanguage != null }.flatMap { grammarRules(LangTool.createTool(it, state), it) }
state.enabledLanguages.filter { it.jLanguage != null }.flatMap { grammarRules(LangTool.createTool(it, state, TextStyleDomain.Other), it) }
}
fun convert(ids: Set<String>): Set<String> =
@@ -179,7 +253,7 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
*
* Should never be called in GrazieStateLifecycle actions
*/
fun get() = service<GrazieConfig>().state
fun get(): State = service<GrazieConfig>().state
/** Update Grazie config state */
@Synchronized
@@ -194,6 +268,8 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
}
}
data class UserChangedRules(val enabled: Set<String>, val disabled: Set<String>)
class PresentableNameGetter : com.intellij.openapi.components.State.NameGetter() {
override fun get() = GrazieBundle.message("grazie.config.name")
}
@@ -203,7 +279,7 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
override fun getModificationCount(): Long = myModCount.get()
override fun getState() = myState
override fun getState(): State = myState
override fun loadState(state: State) {
myModCount.incrementAndGet()
@@ -224,7 +300,7 @@ class GrazieConfig : PersistentStateComponent<GrazieConfig.State>, ModificationT
update { state ->
val oxford = get().useOxfordSpelling
if (oxford) {
state.copy(userDisabledRules = state.userDisabledRules - oxfordSpellingLtRules.toSet())
state.copy(userDisabledRules = state.userDisabledRules - oxfordSpellingLtRules)
}
else {
state.copy(userDisabledRules = state.userDisabledRules + oxfordSpellingLtRules)
@@ -2,16 +2,19 @@ package com.intellij.grazie.detection
import ai.grazie.nlp.langs.Language
import ai.grazie.nlp.langs.alphabet.Alphabet
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.jlanguage.Lang
fun Lang.toLanguage() = Language.values().find { it.iso == this.iso }!!
fun Lang.toLanguage(): Language = Language.entries.find { it.iso == this.iso }!!
/** Note that it will return SOME dialect */
fun Language.toLang() = Lang.values().find { it.iso == this.iso }!!
fun Language.toLang(): Lang = Lang.entries.find { it.iso == this.iso }!!
fun Language.toLangOrNull(): Lang? {
return Lang.values().find { it.iso == this.iso }
}
fun Language.toAvailableLang(): Lang = GrazieConfig.get().availableLanguages.find { it.iso == this.iso }!!
fun Language.toAvailableLangOrNull(): Lang? = GrazieConfig.get().availableLanguages.find { it.iso == this.iso }
fun Language.toLangOrNull(): Lang? = Lang.entries.find { it.iso == this.iso }
val Language.hasWhitespaces: Boolean
get() = alphabet.group != Alphabet.Group.ASIAN
@@ -8,6 +8,8 @@ import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.jlanguage.LangTool
import com.intellij.grazie.text.*
import com.intellij.grazie.utils.NaturalTextDetector
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.grazie.utils.getTextDomain
import com.intellij.grazie.utils.trimToNull
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.progress.ProcessCanceledException
@@ -44,7 +46,7 @@ open class LanguageToolChecker : ExternalTextChecker() {
override fun getRules(locale: Locale): Collection<Rule> {
val language = Languages.getLanguageForLocale(locale)
val lang = Lang.entries.find { it.jLanguage == language } ?: return emptyList()
return grammarRules(LangTool.getTool(lang), lang)
return grammarRules(LangTool.getTool(lang, TextStyleDomain.Other), lang)
}
@OptIn(DelicateCoroutinesApi::class)
@@ -72,7 +74,7 @@ open class LanguageToolChecker : ExternalTextChecker() {
}
private fun collectLanguageToolProblems(extracted: TextContent, text: String, lang: Lang): List<Problem> {
val tool = LangTool.getTool(lang)
val tool = LangTool.getTool(lang, extracted.getTextDomain())
val sentences = tool.sentenceTokenize(text)
if (sentences.any { it.length > 1000 }) {
return emptyList()
@@ -5,6 +5,7 @@ import com.intellij.grazie.ide.ui.components.utils.html
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.jlanguage.LangTool
import com.intellij.grazie.text.Rule
import com.intellij.grazie.utils.TextStyleDomain
import kotlinx.html.*
import org.languagetool.JLanguageTool
import org.languagetool.rules.Categories
@@ -18,7 +19,7 @@ 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)) {
override fun isEnabledByDefault(): Boolean = LangTool.isRuleEnabledByDefault(lang, ltRule.id)
override fun isEnabledByDefault(domain: TextStyleDomain): Boolean = LangTool.isRuleEnabledByDefault(lang, ltRule.id, domain)
override fun getUrl(): URL? = similarLtRules.map { it.url }.toSet().singleOrNull()
@@ -18,7 +18,7 @@ import com.intellij.internal.statistic.service.fus.collectors.ApplicationUsagesC
import com.intellij.internal.statistic.utils.getPluginInfo
internal class GrazieFUSState : ApplicationUsagesCollector() {
private val GROUP = EventLogGroup("grazie.state", 9)
private val GROUP = EventLogGroup("grazie.state", 10)
private val ENABLE_LANGUAGE = GROUP.registerEvent(
"enabled.language",
EventFields.Enum("value", LanguageISO::class.java) { it.name.lowercase() }
@@ -100,7 +100,7 @@ internal class GrazieFUSState : ApplicationUsagesCollector() {
val connectionType = GrazieCloudConnector.EP_NAME.extensionList.firstNotNullOfOrNull { it.connectionType() } ?: Processing.Local
metrics.add(PROCESSING.metric(connectionType))
if (state.styleProfile != DEFAULT_STATE.styleProfile) {
metrics.add(WRITING_STYLE.metric(state.textStyle.id.uppercase()))
metrics.add(WRITING_STYLE.metric(state.getTextStyle().id.uppercase()))
}
if (state.autoFix != DEFAULT_STATE.autoFix) {
metrics.add(AUTO_FIX.metric(state.autoFix))
@@ -42,9 +42,7 @@ class GrazieRuleSettingsAction(private val ruleName: String, private val rule: R
navigatable.navigate(true)
ok = true
} else {
val configurable = GrazieConfigurable()
configurable.selectRule(rule.globalId)
ok = ShowSettingsUtil.getInstance().editConfigurable(project, configurable)
ok = ShowSettingsUtil.getInstance().editConfigurable(project, GrazieConfigurable())
}
val result = if (!ok) "canceled" else analyzeStateChange(state1, GrazieConfig.get())
@@ -15,13 +15,6 @@ internal interface GrazieUIComponent {
/** Applies changes from component to passed state of GrazieConfig and returns new version */
fun apply(state: GrazieConfig.State): GrazieConfig.State
/** View-only components, that can not be modified somehow */
interface ViewOnly : GrazieUIComponent {
override fun isModified(state: GrazieConfig.State) = false
override fun apply(state: GrazieConfig.State) = state
override fun reset(state: GrazieConfig.State) {}
}
/** Components, that change representation, but delegate actual data handing to `impl` */
interface Delegating : GrazieUIComponent {
val impl: GrazieUIComponent
@@ -1,30 +1,35 @@
package com.intellij.grazie.ide.ui.configurable
import ai.grazie.nlp.langs.Language
import ai.grazie.nlp.langs.utils.englishName
import ai.grazie.nlp.langs.utils.nativeName
import ai.grazie.rules.Rule
import ai.grazie.rules.settings.RuleSetting
import ai.grazie.rules.settings.Setting
import ai.grazie.rules.settings.SettingComponent
import ai.grazie.rules.settings.TextStyle
import ai.grazie.rules.toolkit.LanguageToolkit
import com.intellij.grazie.GrazieBundle
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.ide.ui.grammar.GrazieConfigurable
import com.intellij.grazie.detection.toLanguage
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.rule.SentenceBatcher
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.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.ide.DataManager
import com.intellij.openapi.Disposable
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.ConfigurableWithId
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.options.ex.Settings
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
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
@@ -36,89 +41,125 @@ import com.intellij.util.IconUtil
import com.intellij.util.ui.JBEmptyBorder
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.JBUI.Borders
import com.intellij.util.ui.UIUtil.FontSize
import com.intellij.util.ui.update.UiNotifyConnector
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Image
import java.awt.Rectangle
import java.net.URL
import javax.swing.JComponent
import javax.swing.JEditorPane
import javax.swing.JPanel
import javax.swing.SwingUtilities
import javax.swing.*
import javax.swing.ScrollPaneConstants.*
import javax.swing.event.DocumentEvent
import javax.swing.event.HyperlinkEvent
class StyleConfigurable:
BoundConfigurable(GrazieBundle.message("grazie.settings.style.configurable.name"), null),
ConfigurableWithId, Configurable.NoScroll { //todo searchable?
class StyleConfigurable : BoundConfigurable(GrazieBundle.message("grazie.settings.grammar.tabs.rules"), null), Disposable, Configurable.NoScroll {
private var focusedControl: JComponent? = null
private var langData = LinkedHashMap<Language, LangData>()
private val settings: Settings = Settings()
private val langComboModel = CollectionComboBoxModel(ArrayList<Language>())
private lateinit var langCombo: ComboBox<Language>
private val filterComponent: SearchTextField = SearchTextField(false).also {
it.textEditor.emptyText.text = GrazieBundle.message("grazie.settings.style.search.placeholder")
it.textEditor.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
updateFilter()
settings.updateFilter(textStyle, langComboModel.selected!!, filterComponent.text)
}
})
it.border = JBEmptyBorder(5)
}
private val settingWrapper by lazy {
JPanel(BorderLayout()).also { it.add(langData[Language.ENGLISH]!!.component) }
private val treeWrapper by lazy {
JBSplitter(false, 0.45f).apply {
firstComponent = createScrollTreeComponent()
secondComponent = settings.getTreeSettings(textStyle, Language.ENGLISH).description.component
}
}
private val settingWrapper by lazy {
JPanel(BorderLayout()).also {
it.add(settings.getFeaturedSettings(textStyle, Language.ENGLISH)!!.component)
it.maximumSize = Dimension(Int.MAX_VALUE, it.preferredSize.height)
}
}
private val separator by lazy {
createTitledSeparator(GrazieBundle.message("grazie.settings.style.rules.other"), IntelliJSpacingConfiguration())
}
private lateinit var domainComboBox: ComboBox<TextStyleDomain>
private lateinit var styleProfileCombo: ComboBox<TextStyle>
private lateinit var langCombo: ComboBox<Language>
private lateinit var styleRowVisibleUpdater: ((Boolean) -> Unit)
private val config get() = GrazieConfig.get()
private var styleProfile: TextStyle
get() = config.textStyle
set(value) = GrazieConfig.update { it.copy(styleProfile = value.id) }
private val textStyle
get(): TextStyle {
val domain = domainComboBox.selected!!
if (domain == TextStyleDomain.Other) return styleProfileCombo.selected!!
return TextStyle.styles(RuleIdeClient.INSTANCE).find { it.id == domain.name }!!
}
override fun getId() = ID
private var otherDomainStyle: TextStyle
get() = config.getTextStyle()
set(value) {
GrazieConfig.update { it.copy(styleProfile = value.id) }
}
@Suppress("UnstableApiUsage")
private val component by lazy {
ruleLanguages.forEach { addLanguage(it) }
val component: DialogPanel by lazy {
loadLanguages()
val userTextStyle = GrazieConfig.get().getTextStyle()
panel {
row(GrazieBundle.message("grazie.settings.writing.style.text")) {
styleProfileCombo = writingStyleComboBox()
.widthGroup("TopCombo")
.bindItem(::styleProfile.toNullableProperty())
.whenItemSelectedFromUi { profile ->
for (data in langData.values) {
data.component.loadState(data.component.state, profile)
row {
comment(GrazieBundle.message("grazie.settings.writing.style.hint"))
}
row {
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!!)
}
.component
domainComboBox.selectedItem = TextStyleDomain.Other
val styleLabelCell = label(GrazieBundle.message("grazie.settings.writing.style.text"))
val styleComboCell = writingStyleComboBox()
.applyIfEnabled()
.bindItem(::otherDomainStyle.toNullableProperty())
.whenItemSelectedFromUi { textStyle ->
if (domainComboBox.selected == TextStyleDomain.Other) {
selectTextStyle(textStyle, langComboModel.selected!!)
settings.reset(GrazieConfig.get())
selectLanguage(textStyle, langComboModel.selected!!)
}
}
.component
settings.addTextStyle(userTextStyle, Language.ENGLISH, filterComponent)
styleProfileCombo = styleComboCell.component
styleProfileCombo.selectedItem = userTextStyle
styleRowVisibleUpdater = { visible ->
styleLabelCell.visible(visible)
styleComboCell.visible(visible)
styleComboCell.enabled(visible)
}
styleRowVisibleUpdater.invoke(domainComboBox.selectedItem == TextStyleDomain.Other)
}
row(GrazieBundle.message("grazie.settings.style.language.chooser.label")) {
langCombo = comboBox(
langComboModel,
SimpleListCellRenderer.create { label, lang, _ -> label.text = lang.nativeName }
)
row(GrazieBundle.message("grazie.settings.language.chooser.label")) {
langCombo = comboBox(langComboModel, SimpleListCellRenderer.create { label, lang, _ -> label.text = lang.nativeName })
.widthGroup("TopCombo")
.whenItemSelectedFromUi {lang -> languageChanged(lang) }
.component
selectLanguage(Language.ENGLISH)
trackNewLanguageAddition()
link(GrazieBundle.message("grazie.settings.style.configure.all.rules.link")) {
val settings = ideSettings()
val configurable = settings?.find(GrazieConfigurable::class.java)
if (configurable != null) {
settings.select(configurable)
} else {
ShowSettingsUtil.getInstance().editConfigurable(settingWrapper, GrazieConfigurable())
.whenItemSelectedFromUi { language ->
settings.addTextStyle(textStyle, language, filterComponent)
selectLanguage(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)
}
}
.component
selectLanguage(userTextStyle, Language.ENGLISH)
trackNewLanguageAddition()
}
row {
@@ -126,40 +167,233 @@ class StyleConfigurable:
}
row {
scrollCell(settingWrapper).resizableColumn().align(Align.FILL)
val content = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
add(settingWrapper)
add(separator)
add(treeWrapper)
add(Box.createVerticalGlue())
}
val scroll = ScrollPaneFactory.createScrollPane(content, VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_NEVER)
cell(scroll).resizableColumn().align(Align.FILL)
resizableRow()
}
}
treeWrapper.minimumSize = JBUI.size(150, 200)
treeWrapper.maximumSize = Dimension(Int.MAX_VALUE, Int.MAX_VALUE)
treeWrapper.preferredSize = JBUI.size(-1, 300)
treeWrapper.setHonorComponentsMinimumSize(true)
settings.getTreeSettings(textStyle, Language.ENGLISH).description.listener(Language.ENGLISH)
}.also { it.border = Borders.empty() }
}
override fun createPanel(): DialogPanel = component
override fun isModified(): Boolean = super<BoundConfigurable>.isModified || settings.isModified()
override fun apply() {
super.apply()
settings.apply()
}
override fun reset() {
super.reset()
settings.reset(GrazieConfig.get())
}
override fun dispose() {
disposeUIResources()
}
private fun selectTextStyle(textStyle: TextStyle, language: Language) {
settings.addTextStyle(textStyle, language, filterComponent)
repaintSettings(textStyle, language)
}
private fun selectLanguage(textStyle: TextStyle, language: Language) {
settings.addLanguage(textStyle, language, filterComponent)
langCombo.selectedItem = language
repaintSettings(textStyle, language)
}
private fun repaintSettings(textStyle: TextStyle, language: Language) {
val hasFeaturedVisible = language in ruleEngineLanguages
settingWrapper.isVisible = hasFeaturedVisible
if (hasFeaturedVisible) {
settingWrapper.removeAll()
settingWrapper.add(settings.getFeaturedSettings(textStyle, language)!!.component)
settingWrapper.maximumSize = Dimension(Int.MAX_VALUE, settingWrapper.preferredSize.height)
settingWrapper.repaint()
}
treeWrapper.firstComponent = createScrollTreeComponent()
treeWrapper.secondComponent = settings.getTreeSettings(textStyle, language).description.component
treeWrapper.repaint()
}
private fun trackNewLanguageAddition() {
GrazieConfig.subscribe(disposable!!) {
for (lang in ruleLanguages) {
if (lang !in langData) {
addLanguage(lang)
GrazieConfig.subscribe(this) {
if (loadLanguages()) {
settings.clear()
settings.addTextStyle(textStyle, Language.ENGLISH, filterComponent)
}
}
}
private fun loadLanguages(): Boolean {
val langs = GrazieConfig.get().availableLanguages.map { it.toLanguage() }.sortedBy { it.englishName }
if (langComboModel.items == langs) return false
langComboModel.removeAll()
langComboModel.add(langs)
return true
}
override fun getDisplayName(): @NlsContexts.ConfigurableName String = ""
override fun getPreferredFocusedComponent(): JComponent? {
return focusedControl ?: super.getPreferredFocusedComponent()
}
private fun createScrollTreeComponent(): JScrollPane {
return ScrollPaneFactory.createScrollPane(
settings.getTreeSettings(textStyle, Language.ENGLISH).tree,
VERTICAL_SCROLLBAR_AS_NEEDED,
HORIZONTAL_SCROLLBAR_AS_NEEDED
)
}
companion object {
@JvmStatic
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 }
@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
}
}
}
}
override fun canNavigate() = true
override fun canNavigateToSource() = false
}
}
}
}
data class Settings(
val featuredSettings: MutableMap<String, MutableMap<Language, FeaturedSettings>> = HashMap(),
private 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 reset(state: GrazieConfig.State) {
featuredSettings.forEach { (domain, featuredSettings) ->
if (isModifiedFeaturedSettings(featuredSettings)) {
featuredSettings.forEach { (language, settings) ->
val textStyle = getTextStyle(domain)
settings.component.loadState(getSettingsState(language, textStyle), textStyle)
settings.resetState = settings.component.state
}
}
}
}
private fun createLinkLabel(text: String, onClick: Runnable): JComponent {
val link = HyperlinkLabel(text)
link.addHyperlinkListener { e: HyperlinkEvent ->
if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) {
onClick.run()
treeSettings.forEach { (_, treesSettings) ->
if (isModifiedTreeSettings(treesSettings)) {
treesSettings.forEach { (_, settings) -> settings.tree.reset(state) }
}
}
link.setFontSize(FontSize.SMALL)
link.updateUI()
return link
}
private fun addLanguage(lang: Language) {
if (lang !in ruleLanguages || SentenceBatcher.findInstalledLTLanguage(lang) == null) return
fun apply() {
treeSettings
.filter { (domainId, treeSettingsMap) -> isModifiedFeaturedSettings(featuredSettings[domainId]!!) || isModifiedTreeSettings(treeSettingsMap) }
.forEach { (domainId, treeSettingsMap) ->
val domain = getTextStyle(domainId).getTextDomain()
val userEnabledRules = HashSet<String>()
val userDisabledRules = HashSet<String>()
val parameters = HashMap<Language, Map<String, String>>()
val toolkit = LanguageToolkit.forLanguage(lang)
featuredSettings[domainId]?.forEach { (language, settings) ->
val prefix = Rule.globalIdPrefix(language)
val settingsState = settings.component.state
settings.resetState = settingsState
for (id in settingsState.enabledRules) {
userEnabledRules.add(prefix + id)
userDisabledRules.remove(prefix + id)
}
for (id in settingsState.disabledRules) {
userEnabledRules.remove(prefix + id)
userDisabledRules.add(prefix + id)
}
parameters[language] = settingsState.paramValues
}
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()
addLanguage(textStyle, language, filterComponent)
}
fun addLanguage(textStyle: TextStyle, language: Language, filterComponent: SearchTextField) {
val featuredSettingsPerLanguage = featuredSettings[textStyle.id]!!
val treeSettingsPerLanguage = treeSettings[textStyle.id]!!
if (language in featuredSettingsPerLanguage || language in treeSettingsPerLanguage) return
val domain = textStyle.getTextDomain()
val description = GrazieDescriptionComponent()
val tree = GrazieTreeComponent(description.listener, language, domain, filterComponent)
treeSettingsPerLanguage[language] = TreeSettings(description, tree)
treeSettingsPerLanguage[language]!!.tree.reset(GrazieConfig.get())
if (language !in ruleEngineLanguages) return
val toolkit = LanguageToolkit.forLanguage(language)
val spacing = IntelliJSpacingConfiguration()
val ui = object : SettingComponent.UI {
override fun getExamplePrefix(): String = GrazieBundle.message("grazie.settings.style.configurable.example.prefix")
@@ -185,18 +419,11 @@ class StyleConfigurable:
override fun createCollapseComponent(examplesOnly: Boolean, doCollapse: Runnable): JComponent {
val text =
if (examplesOnly) GrazieBundle.message("grazie.settings.style.configurable.collapse.examples.link")
else GrazieBundle.message("grazie.settings.style.configurable.collapse.link")
else GrazieBundle.message("grazie.settings.style.configurable.collapse.link")
return createLinkLabel(text, doCollapse)
}
override fun createGroupHeader(name: String): JComponent {
val title = JBLabel(name)
val separator = object : TitledSeparator(title.text) {
override fun createLabel(): JBLabel = title
}
separator.border = JBEmptyBorder(spacing.verticalMediumGap, 0, spacing.verticalSmallGap, 0)
return separator
}
override fun createGroupHeader(@NlsContexts.Label name: String): JComponent = createTitledSeparator(name, spacing)
override fun customizeSettingSection(setting: Setting, section: JComponent) {
section.border = JBEmptyBorder(0, spacing.horizontalIndent, spacing.verticalComponentGap, 0)
@@ -207,159 +434,101 @@ class StyleConfigurable:
pane.foreground = JBUI.CurrentTheme.ContextHelp.FOREGROUND
}
}
val comp = SettingComponent(toolkit, RuleIdeClient.INSTANCE, ui)
val component = SettingComponent(toolkit, RuleIdeClient.INSTANCE, ui)
val prefix = Rule.globalIdPrefix(lang)
val affectedRules = affectedSettings(toolkit)
.filterIsInstance<RuleSetting>()
.map { prefix + it.rule.id }
.toSet()
langData[lang] = LangData(comp, SettingComponent.SettingState.UNCHANGED, affectedRules)
langComboModel.add(lang)
val settingState = getSettingsState(language, textStyle)
component.loadState(settingState, textStyle)
featuredSettingsPerLanguage[language] = FeaturedSettings(component, settingState)
}
private fun updateFilter() {
langData[langComboModel.selected!!]!!.component.filter(filterComponent.text)
fun updateFilter(textStyle: TextStyle, language: Language, option: String) {
featuredSettings[textStyle.id]!![language]!!.component.filter(option)
treeSettings[textStyle.id]!![language]!!.tree.filter(option)
}
private fun selectLanguage(lang: Language) {
langCombo.selectedItem = lang
languageChanged(lang)
fun clear() {
featuredSettings.clear()
treeSettings.clear()
}
private fun languageChanged(lang: Language) {
updateFilter()
settingWrapper.removeAll()
settingWrapper.add(langData[lang]!!.component)
settingWrapper.repaint()
private fun isModifiedFeaturedSettings(featuredSettings: MutableMap<Language, FeaturedSettings>): Boolean {
return featuredSettings.any { it.value.component.state != it.value.resetState }
}
override fun apply() {
val prev = GrazieConfig.get()
super.apply()
val userEnabledRules = HashSet(GrazieConfig.get().userEnabledRules)
val userDisabledRules = HashSet(GrazieConfig.get().userDisabledRules)
val params = HashMap(GrazieConfig.get().parameters)
for ((lang, data) in langData) {
val prefix = Rule.globalIdPrefix(lang)
val state = data.component.state
data.resetState = state
val affectedIds = data.affectedGlobalRuleIds
userEnabledRules.removeIf { it.startsWith(prefix) && it in affectedIds }
userDisabledRules.removeIf { it.startsWith(prefix) && it in affectedIds }
for (id in state.enabledRules) {
userEnabledRules.add(prefix + id)
userDisabledRules.remove(prefix + id)
}
for (id in state.disabledRules) {
userEnabledRules.remove(prefix + id)
userDisabledRules.add(prefix + id)
}
params[lang] = state.paramValues
}
GrazieConfig.update { it.copy(parameters = params) }
val liteConfigurable = ideSettings()?.find(GrazieConfigurable::class.java)
if (userEnabledRules != GrazieConfig.get().userEnabledRules ||
userDisabledRules != GrazieConfig.get().userDisabledRules) {
GrazieConfig.update { it.copy(userEnabledRules = userEnabledRules, userDisabledRules = userDisabledRules) }
liteConfigurable?.reset()
}
if (GrazieConfig.get().styleProfile != prev.styleProfile) {
liteConfigurable?.ruleEnablednessChanged(GrazieConfig.get())
}
private fun isModifiedTreeSettings(treeSettings: MutableMap<Language, TreeSettings>): Boolean {
return treeSettings.any { it.value.tree.isModified(GrazieConfig.get()) }
}
}
override fun isModified(): Boolean {
return super<BoundConfigurable>.isModified() ||
langData.values.any { it.component.state != it.resetState }
}
data class FeaturedSettings(
val component: SettingComponent,
var resetState: SettingComponent.SettingState,
)
override fun reset() {
super<BoundConfigurable>.reset()
data class TreeSettings(
val description: GrazieDescriptionComponent,
val tree: GrazieTreeComponent,
)
val currentStyle = styleProfileCombo.selectedItem as TextStyle
private fun getSettingsState(language: Language, textStyle: TextStyle): SettingComponent.SettingState {
val state = GrazieConfig.get()
val userEnabledRules = GrazieConfig.get().userEnabledRules
val userDisabledRules = GrazieConfig.get().userDisabledRules
for ((lang, data) in langData) {
val prefix = Rule.globalIdPrefix(lang)
val langState = SettingComponent.SettingState(
GrazieConfig.get().parameters[lang] ?: emptyMap(),
userEnabledRules.filter { it.startsWith(prefix) }.map { it.substring(prefix.length) }.toSet(),
userDisabledRules.filter { it.startsWith(prefix) }.map { it.substring(prefix.length) }.toSet(),
)
data.component.loadState(langState, currentStyle)
data.resetState = data.component.state
}
}
override fun getPreferredFocusedComponent(): JComponent? {
return focusedControl ?: super<BoundConfigurable>.getPreferredFocusedComponent()
}
private fun ideSettings() = DataManager.getInstance().getDataContext(settingWrapper).getData(Settings.KEY)
companion object {
internal const val ID = "reference.settings.grazie.pro.style"
val ruleLanguages = listOf(Language.ENGLISH, Language.GERMAN, Language.RUSSIAN, Language.UKRAINIAN)
@JvmStatic
fun affectedSettings(toolkit: LanguageToolkit): List<Setting> = toolkit.getSettings(RuleIdeClient.INSTANCE).flatMap { it.settings }
@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()
for ((lang, data) in conf.langData) {
val settingComponent = data.component.findParentSettingComponent(setting)
val paramComponent = data.component.findOwnSettingComponent(setting)
if (paramComponent != null && settingComponent != null) {
conf.focusedControl = paramComponent
conf.selectLanguage(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
}
}
}
}
override fun canNavigate() = true
override fun canNavigateToSource() = false
}
}
}
private data class LangData(
val component: SettingComponent,
var resetState: SettingComponent.SettingState,
var affectedGlobalRuleIds: Set<String>
val prefix = Rule.globalIdPrefix(language)
val domain = textStyle.getTextDomain()
val (userEnabledRules, userDisabledRules) = state.getUserChangedRules(domain)
val parameters = if (domain != TextStyleDomain.Other) state.parametersPerDomain[domain] else state.parameters
return SettingComponent.SettingState(
parameters?.get(language) ?: emptyMap(),
userEnabledRules.filter { it.startsWith(prefix) }.map { it.substring(prefix.length) }.toSet(),
userDisabledRules.filter { it.startsWith(prefix) }.map { it.substring(prefix.length) }.toSet(),
)
}
fun Row.writingStyleComboBox() = comboBox(
CollectionComboBoxModel(TextStyle.styles(RuleIdeClient.INSTANCE)),
private fun getTextStyle(textStyleId: String): TextStyle {
return TextStyle.styles(RuleIdeClient.INSTANCE).find { it.id == textStyleId } ?: TextStyle.Unspecified
}
private fun createTitledSeparator(@NlsContexts.Label name: String, spacing: IntelliJSpacingConfiguration): TitledSeparator {
val title = JBLabel(name)
return createTitledSeparator(title, spacing)
}
private fun createTitledSeparator(title: JBLabel, spacing: IntelliJSpacingConfiguration): TitledSeparator {
val separator = object : TitledSeparator(title.text) {
override fun createLabel(): JBLabel = title
}
separator.border = JBEmptyBorder(spacing.verticalMediumGap, 0, spacing.verticalSmallGap, 0)
separator.maximumSize = Dimension(Int.MAX_VALUE, separator.preferredSize.height)
return separator
}
private fun createLinkLabel(@NlsContexts.LinkLabel text: String, onClick: Runnable): JComponent {
val link = HyperlinkLabel(text)
link.addHyperlinkListener { e: HyperlinkEvent ->
if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) {
onClick.run()
}
}
link.setFontSize(FontSize.SMALL)
link.updateUI()
return link
}
private fun Row.writingStyleComboBox() = comboBox(
CollectionComboBoxModel(getOtherDomainStyles()),
SimpleListCellRenderer.create { label, value, _ ->
label.text = GrazieBundle.messageOrNull("grazie.settings.style.profile.display.${value.id}")
?: NameUtil.splitNameIntoWords(value.id).joinToString(" ")
}
)
private fun Row.domainComboBox() = comboBox(
CollectionComboBoxModel(TextStyleDomain.entries),
SimpleListCellRenderer.create { label, value, _ ->
label.text = GrazieBundle.messageOrNull("grazie.settings.domain.profile.display.$value")
}
)
@Suppress("UNCHECKED_CAST")
private val <T> ComboBox<T>.selected: T?
get() = this.selectedItem as? T
@@ -11,55 +11,19 @@ import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.options.Configurable.WithEpDependencies
import com.intellij.openapi.options.ConfigurableBase
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.util.ui.tree.TreeUtil
import javax.swing.JComponent
class GrazieConfigurable :
ConfigurableBase<GrazieSettingsPanel, GrazieConfig>(
"reference.settingsdialog.project.grazie", GraziePlugin.settingsPageName, "reference.settings.ide.settings.grammar"),
WithEpDependencies,
SearchableConfigurable
{
SearchableConfigurable {
private val ui: GrazieSettingsPanel by lazy { GrazieSettingsPanel() }
override fun getSettings() = service<GrazieConfig>()
override fun createUi(): GrazieSettingsPanel = ui
override fun getPreferredFocusedComponent(): JComponent? {
if (ui.component.selectedComponent == ui.rules.component) {
return ui.rules.impl
}
return super<ConfigurableBase>.getPreferredFocusedComponent()
}
override fun enableSearch(option: String?): Runnable? {
if (option != null) {
return Runnable {
ui.component.selectedComponent = ui.rules.component
ui.rules.impl.filter(option)
}
}
return null
}
internal fun selectRule(globalId: String) {
ui.component.selectedComponent = ui.rules.component
val tree = ui.rules.impl
val ruleNode = (tree.model.root as GrazieRulesTreeNode).findRuleNode(globalId)
if (ruleNode != null) {
TreeUtil.selectNode(tree, ruleNode)
}
}
// used in Grazie Pro
@Suppress("unused", "SpellCheckingInspection")
fun ruleEnablednessChanged(state: GrazieConfig.State) {
val tree = ui.rules.impl
(tree.model.root as GrazieRulesTreeNode).resetMark(tree.apply(state))
}
override fun getDependencies(): Collection<BaseExtensionPointName<*>> {
return setOf(LanguageGrammarChecking.EP_NAME,
ExtensionPointName("com.intellij.grazie.textExtractor"),
@@ -3,34 +3,34 @@ package com.intellij.grazie.ide.ui.grammar
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.ide.ui.components.dsl.msg
import com.intellij.grazie.ide.ui.configurable.StyleConfigurable
import com.intellij.grazie.ide.ui.grammar.tabs.exceptions.GrazieExceptionsTab
import com.intellij.grazie.ide.ui.grammar.tabs.rules.GrazieRulesTab
import com.intellij.grazie.ide.ui.grammar.tabs.scope.GrazieScopeTab
import com.intellij.openapi.Disposable
import com.intellij.openapi.options.ConfigurableUi
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBTabbedPane
import com.intellij.util.ui.JBUI
import javax.swing.JComponent
class GrazieSettingsPanel : ConfigurableUi<GrazieConfig>, Disposable {
private val scope = GrazieScopeTab()
internal val rules = GrazieRulesTab()
internal val rules = StyleConfigurable()
private val exceptions = GrazieExceptionsTab()
override fun isModified(settings: GrazieConfig): Boolean = rules.isModified(settings.state) ||
override fun isModified(settings: GrazieConfig): Boolean = rules.isModified ||
scope.isModified(settings.state) ||
exceptions.isModified(settings.state)
override fun apply(settings: GrazieConfig) {
rules.apply()
GrazieConfig.update { state ->
exceptions.apply(scope.apply(rules.apply(state)))
exceptions.apply(scope.apply(state))
}
rules.reset(settings.state)
}
override fun reset(settings: GrazieConfig) {
rules.reset(settings.state)
rules.reset()
scope.reset(settings.state)
exceptions.reset(settings.state)
}
@@ -44,6 +44,7 @@ class GrazieSettingsPanel : ConfigurableUi<GrazieConfig>, Disposable {
override fun getComponent(): JComponent = component
override fun dispose() = rules.dispose()
override fun dispose() {
Disposer.dispose(rules)
}
}
@@ -1,46 +0,0 @@
package com.intellij.grazie.ide.ui.grammar.tabs.rules
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.ide.msg.GrazieInitializerManager
import com.intellij.grazie.ide.msg.GrazieStateLifecycle
import com.intellij.grazie.ide.ui.components.GrazieUIComponent
import com.intellij.grazie.ide.ui.components.dsl.panel
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.ide.ui.grammar.tabs.rules.component.allRules
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.util.Disposer
import com.intellij.ui.layout.migLayout.*
import com.intellij.util.ui.JBUI
import net.miginfocom.layout.AC
import net.miginfocom.layout.CC
import net.miginfocom.swing.MigLayout
internal class GrazieRulesTab : GrazieUIComponent.Delegating, Disposable {
private val description = GrazieDescriptionComponent()
override val impl = GrazieTreeComponent(description.listener)
override val component = panel(MigLayout(createLayoutConstraints(), AC().grow(), AC().grow())) {
border = JBUI.Borders.empty()
add(impl.component, CC().grow().width("45%").minWidth("250px"))
add(description.component, CC().grow().width("55%"))
impl.reset(GrazieConfig.get())
}
// update the tree on language list change in Natural Languages configurable
private val connection = service<GrazieInitializerManager>().register(object : GrazieStateLifecycle {
override fun update(prevState: GrazieConfig.State, newState: GrazieConfig.State) {
if (prevState.enabledLanguages != newState.enabledLanguages) {
impl.resetTreeModel(allRules(newState))
}
}
})
override fun dispose() {
Disposer.dispose(impl)
Disposer.dispose(connection)
}
}
@@ -1,12 +1,13 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.grazie.ide.ui.grammar.tabs.rules.component
import ai.grazie.nlp.langs.Language
import ai.grazie.nlp.langs.utils.nativeName
import com.intellij.grazie.ide.ui.components.dsl.msg
import com.intellij.grazie.ide.ui.components.dsl.padding
import com.intellij.grazie.ide.ui.components.dsl.panel
import com.intellij.grazie.ide.ui.components.utils.GrazieLinkLabel
import com.intellij.grazie.ide.ui.components.utils.html
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.text.Rule
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.util.NlsSafe
@@ -66,7 +67,7 @@ class GrazieDescriptionComponent {
@NlsSafe
private fun getDescriptionPaneContent(meta: Any): String = when (meta) {
is Lang -> html { unsafe { +msg("grazie.settings.grammar.rule.language.template", meta.nativeName) } }
is Language -> html { unsafe { +msg("grazie.settings.grammar.rule.language.template", meta.nativeName) } }
is String -> html { unsafe { +msg("grazie.settings.grammar.rule.category.template", meta) } }
is Rule -> meta.description
else -> ""
@@ -1,6 +1,7 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.grazie.ide.ui.grammar.tabs.rules.component
import ai.grazie.nlp.langs.Language
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.ide.ui.components.GrazieUIComponent
import com.intellij.grazie.ide.ui.components.dsl.panel
@@ -10,6 +11,8 @@ import com.intellij.grazie.ide.ui.grammar.tabs.rules.component.rules.GrazieRules
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.text.Rule
import com.intellij.grazie.text.TextChecker
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.grazie.utils.getAffectedGlobalRules
import com.intellij.ide.CommonActionsManager
import com.intellij.ide.DefaultTreeExpander
import com.intellij.openapi.Disposable
@@ -20,14 +23,18 @@ import com.intellij.util.ui.JBUI
import com.intellij.util.ui.tree.TreeUtil
import org.jetbrains.annotations.ApiStatus
import java.awt.BorderLayout
import javax.swing.ScrollPaneConstants
import javax.swing.tree.DefaultTreeModel
internal class GrazieTreeComponent(onSelectionChanged: (meta: Any) -> Unit) : CheckboxTree(GrazieRulesTreeCellRenderer(), GrazieRulesTreeNode()),
Disposable, GrazieUIComponent {
class GrazieTreeComponent(
onSelectionChanged: (meta: Any) -> Unit,
private val language: Language,
private val domain: TextStyleDomain,
private val filter: SearchTextField,
) : CheckboxTree(GrazieRulesTreeCellRenderer(), GrazieRulesTreeNode(domain)),
Disposable, GrazieUIComponent {
private val disabledRules = hashSetOf<String>()
private val enabledRules = hashSetOf<String>()
private val filterComponent: GrazieRulesTreeFilter = GrazieRulesTreeFilter(this)
private val filterComponent = GrazieRulesTreeFilter(this, language)
init {
selectionModel.addTreeSelectionListener { event ->
@@ -42,7 +49,7 @@ internal class GrazieTreeComponent(onSelectionChanged: (meta: Any) -> Unit) : Ch
val id = meta.globalId
enabledRules.remove(id)
disabledRules.remove(id)
if (node.isChecked != meta.isEnabledByDefault) {
if (node.isChecked != meta.isEnabledByDefault(domain)) {
(if (node.isChecked) enabledRules else disabledRules).add(id)
}
}
@@ -69,76 +76,66 @@ internal class GrazieTreeComponent(onSelectionChanged: (meta: Any) -> Unit) : Ch
toolbar.setTargetComponent(this@tree)
add(toolbar.component, BorderLayout.WEST)
}
add(filterComponent, BorderLayout.CENTER)
}
panel(constraint = BorderLayout.CENTER) {
add(ScrollPaneFactory.createScrollPane(this@GrazieTreeComponent,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER))
}
}
}
override fun isModified(state: GrazieConfig.State): Boolean {
return state.userEnabledRules != enabledRules || state.userDisabledRules != disabledRules
val (userEnabledRules, userDisabledRules) = state.getUserChangedRules(domain)
return userEnabledRules != enabledRules || userDisabledRules != disabledRules
}
override fun reset(state: GrazieConfig.State) {
enabledRules.clear(); enabledRules.addAll(state.userEnabledRules)
disabledRules.clear(); disabledRules.addAll(state.userDisabledRules)
filterComponent.filter()
if (isSelectionEmpty) setSelectionRow(0)
val (userEnabledRules, userDisabledRules) = state.getUserChangedRules(domain)
enabledRules.clear(); enabledRules.addAll(userEnabledRules)
disabledRules.clear(); disabledRules.addAll(userDisabledRules)
filterComponent.filter(filter.text)
}
override fun apply(state: GrazieConfig.State): GrazieConfig.State {
return state.copy(
userEnabledRules = HashSet(enabledRules),
userDisabledRules = HashSet(disabledRules)
)
return state.updateUserRules(domain, enabledRules, disabledRules)
}
override fun dispose() {
filterComponent.dispose()
override fun dispose() {}
@JvmOverloads
fun filter(filterText: String? = filter.text) {
filterComponent.filter(filterText)
}
fun filter(str: String) {
filterComponent.filter = str
filterComponent.filter()
}
fun getCurrentFilterString(): String? = filter.text
fun getCurrentFilterString(): String? = filterComponent.filter
fun resetTreeModel(rules: Map<Lang, List<Rule>>) {
val root = GrazieRulesTreeNode()
fun resetTreeModel(rules: List<Rule>) {
val root = GrazieRulesTreeNode(domain)
val model = model as DefaultTreeModel
rules.entries.sortedBy { it.key.nativeName }.forEach { (lang, rules) ->
val langNode = GrazieRulesTreeNode(lang)
model.insertNodeInto(langNode, root, root.childCount)
fun splitIntoCategories(level: Int, rules: List<Rule>, parent: GrazieRulesTreeNode) {
rules.groupBy { it.categories.getOrNull(level) }.entries
.sortedWith(Comparator.comparing({ it.key }, nullsLast(Comparator.comparing { it.lowercase() })))
.forEach { (category, catRules) ->
if (category != null) {
val categoryNode = GrazieRulesTreeNode(category)
model.insertNodeInto(categoryNode, parent, parent.childCount)
splitIntoCategories(level + 1, catRules, categoryNode)
}
else {
catRules.sortedBy { it.presentableName.lowercase() }.forEach { rule ->
model.insertNodeInto(GrazieRulesTreeNode(rule), parent, parent.childCount)
}
fun splitIntoCategories(level: Int, rules: List<Rule>, parent: GrazieRulesTreeNode) {
rules.groupBy { it.categories.getOrNull(level) }.entries
.sortedWith(Comparator.comparing({ it.key }, nullsLast(Comparator.comparing { it.lowercase() })))
.forEach { (category, categoryRules) ->
if (category != null) {
val categoryNode = GrazieRulesTreeNode(domain, category)
model.insertNodeInto(categoryNode, parent, parent.childCount)
splitIntoCategories(level + 1, categoryRules, categoryNode)
}
else {
categoryRules.sortedBy { it.presentableName.lowercase() }.forEach { rule ->
model.insertNodeInto(GrazieRulesTreeNode(domain, rule), parent, parent.childCount)
}
}
}
splitIntoCategories(0, rules, langNode)
}
}
val affectedGlobalRules = getAffectedGlobalRules(language)
splitIntoCategories(
0,
rules.filter { it.globalId !in affectedGlobalRules },
root
)
val state = GrazieConfig.get()
model.setRoot(root)
root.resetMark(apply(GrazieConfig.get()))
root.resetMark(apply(state))
model.nodeChanged(root)
}
}
@@ -156,4 +153,17 @@ fun allRules(state: GrazieConfig.State = GrazieConfig.get()): Map<Lang, List<Rul
}
}
return result
}
}
@ApiStatus.Internal
fun allRules(lang: Lang, state: GrazieConfig.State = GrazieConfig.get()): List<Rule> {
if (lang !in state.enabledLanguages) return emptyList()
val jLanguage = lang.jLanguage
if (jLanguage != null) {
val rules = TextChecker.allCheckers().flatMap { it.getRules(jLanguage.localeWithCountryAndVariant) }
if (rules.isNotEmpty()) {
return rules
}
}
return emptyList()
}
@@ -1,26 +1,36 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.grazie.ide.ui.grammar.tabs.rules.component.rules
import ai.grazie.nlp.langs.Language
import com.intellij.grazie.detection.toAvailableLang
import com.intellij.grazie.ide.ui.grammar.tabs.rules.component.GrazieTreeComponent
import com.intellij.grazie.ide.ui.grammar.tabs.rules.component.allRules
import com.intellij.packageDependencies.ui.TreeExpansionMonitor
import com.intellij.ui.FilterComponent
import com.intellij.util.ui.tree.TreeUtil
import javax.swing.tree.DefaultTreeModel
internal class GrazieRulesTreeFilter(private val tree: GrazieTreeComponent) : FilterComponent("GRAZIE_RULE_FILTER", 10) {
internal class GrazieRulesTreeFilter(
private val tree: GrazieTreeComponent,
private val language: Language,
) {
private val expansionMonitor = TreeExpansionMonitor.install(tree)
override fun filter() {
fun filter(filterText: String?) {
val hadSelection = !tree.selectionPaths.isNullOrEmpty()
expansionMonitor.freeze()
filter(filter)
filterTree(filterText)
(tree.model as DefaultTreeModel).reload()
if (filter.isNullOrBlank()) {
if (filterText.isNullOrBlank()) {
TreeUtil.collapseAll(tree, 0)
expansionMonitor.restore()
if (hadSelection) {
expansionMonitor.restore()
}
else {
expansionMonitor.unfreeze()
}
}
else {
TreeUtil.expandAll(tree)
@@ -28,21 +38,20 @@ internal class GrazieRulesTreeFilter(private val tree: GrazieTreeComponent) : Fi
}
}
private fun filter(filterString: String?) {
private fun filterTree(filterString: String?) {
val lang = language.toAvailableLang()
if (filterString.isNullOrBlank()) {
tree.resetTreeModel(allRules())
tree.resetTreeModel(allRules(lang))
return
}
tree.resetTreeModel(
allRules().map { (lang, rules) ->
lang to rules.filter {
lang.nativeName.contains(filterString, true) ||
it.categories.any { cat -> cat.contains(filterString, true) } ||
it.presentableName.contains(filterString, true) ||
it.searchableDescription.contains(filterString, true)
}
}.toMap().filterValues { it.isNotEmpty() }
)
val rules = allRules(lang)
.filter {
lang.nativeName.contains(filterString, true) ||
it.categories.any { cat -> cat.contains(filterString, true) } ||
it.presentableName.contains(filterString, true) ||
it.searchableDescription.contains(filterString, true)
}
tree.resetTreeModel(rules)
}
}
@@ -4,12 +4,16 @@ package com.intellij.grazie.ide.ui.grammar.tabs.rules.component.rules
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.text.Rule
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.ui.CheckedTreeNode
import com.intellij.ui.JBColor
import com.intellij.ui.SimpleTextAttributes
@Suppress("EqualsOrHashCode")
internal class GrazieRulesTreeNode(userObject: Any? = null) : CheckedTreeNode(userObject) {
internal class GrazieRulesTreeNode(
private val domain: TextStyleDomain,
userObject: Any? = null,
) : CheckedTreeNode(userObject) {
val nodeText: String
get() = when (val meta = userObject) {
is Rule -> meta.presentableName
@@ -26,7 +30,7 @@ internal class GrazieRulesTreeNode(userObject: Any? = null) : CheckedTreeNode(us
private fun differsFromDefault(): Boolean {
val meta = userObject
if (meta is Rule) return meta.isEnabledByDefault != isChecked
if (meta is Rule) return meta.isEnabledByDefault(domain) != isChecked
return children.orEmpty().any { (it as GrazieRulesTreeNode).differsFromDefault() }
}
@@ -38,7 +42,7 @@ internal class GrazieRulesTreeNode(userObject: Any? = null) : CheckedTreeNode(us
fun resetMark(state: GrazieConfig.State): Boolean {
val meta = userObject
if (meta is Rule) {
isChecked = meta.isEnabledInState(state)
isChecked = meta.isEnabledInState(state, domain)
}
else {
isChecked = false
@@ -8,6 +8,7 @@ import com.intellij.grazie.ide.msg.GrazieStateLifecycle
import com.intellij.grazie.jlanguage.broker.GrazieDynamicDataBroker
import com.intellij.grazie.jlanguage.filters.UppercaseMatchFilter
import com.intellij.grazie.jlanguage.hunspell.LuceneHunspellDictionary
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.containers.ContainerUtil
@@ -31,8 +32,8 @@ import kotlin.math.max
import kotlin.math.min
object LangTool : GrazieStateLifecycle {
private val langs: MutableMap<Lang, JLanguageTool> = Collections.synchronizedMap(ContainerUtil.createSoftValueMap())
private val rulesEnabledByDefault = ConcurrentHashMap<Lang, Set<String>>()
private val langs: MutableMap<Lang, MutableMap<TextStyleDomain, JLanguageTool>> = ConcurrentHashMap()
private val rulesEnabledByDefault = ConcurrentHashMap<Lang, MutableMap<TextStyleDomain, Set<String>>>()
private val inappropriateExamples = mapOf(
"DT_NNS_AGREEMENT" to setOf("small children and their mothers", "eternal rest")
)
@@ -49,40 +50,37 @@ object LangTool : GrazieStateLifecycle {
internal fun globalIdPrefix(lang: Lang): String = "LanguageTool." + lang.ltRemote!!.iso.name + "."
fun getTool(lang: Lang): JLanguageTool {
fun getTool(lang: Lang, domain: TextStyleDomain): JLanguageTool {
// this is equivalent to computeIfAbsent, but allows multiple threads to create tools concurrently,
// so that threads can be interrupted (with checkCanceled on their own indicator) instead of waiting on a lock
while (true) {
var tool = langs[lang]
val tools = langs.computeIfAbsent(lang) { Collections.synchronizedMap(ContainerUtil.createSoftValueMap()) }
var tool = tools[domain]
if (tool != null) return tool
val state = GrazieConfig.get()
tool = createTool(lang, state)
tool = createTool(lang, state, domain)
synchronized(langs) {
if (state === GrazieConfig.get()) {
val alreadyComputed = langs[lang]
val alreadyComputed = tools[domain]
if (alreadyComputed != null) return alreadyComputed
langs[lang] = tool
tools[domain] = tool
return tool
}
}
}
}
internal fun createTool(lang: Lang, state: GrazieConfig.State): JLanguageTool {
internal fun createTool(lang: Lang, state: GrazieConfig.State, domain: TextStyleDomain): JLanguageTool {
val jLanguage = lang.jLanguage
require(jLanguage != null) { "Trying to get LangTool for not available language" }
return JLanguageTool(jLanguage, null, ResultCache(10_000)).apply {
setCheckCancelledCallback { ProgressManager.checkCanceled(); false }
addMatchFilter(UppercaseMatchFilter())
val prefix = globalIdPrefix(lang)
val disabledRules = state.userDisabledRules.mapNotNull { if (it.startsWith(prefix)) it.substring(prefix.length) else null }.toSet()
val enabledRules = state.userEnabledRules.mapNotNull { if (it.startsWith(prefix)) it.substring(prefix.length) else null }.toSet()
val (enabledRules, disabledRules) = getRules(lang, state, domain)
enabledRules. forEach { id -> enableRule(id) }
disabledRules.forEach { id -> disableRule(id) }
enabledRules.forEach { id -> enableRule(id) }
fun loadConfigFile(path: String, block: (iso: String, id: String) -> Unit) {
GrazieDynamicDataBroker.getFromResourceDirAsStream(path).use { stream ->
@@ -147,6 +145,22 @@ object LangTool : GrazieStateLifecycle {
return false
}
private fun getRules(lang: Lang, state: GrazieConfig.State, domain: TextStyleDomain): Pair<Set<String>, Set<String>> {
val prefix = globalIdPrefix(lang)
val enabledRules = HashSet<String>()
val disabledRules = HashSet<String>()
if (domain == TextStyleDomain.Other) {
enabledRules.addAll( state.userEnabledRules.mapNotNull { if (it.startsWith(prefix)) it.substring(prefix.length) else null })
disabledRules.addAll(state.userDisabledRules.mapNotNull { if (it.startsWith(prefix)) it.substring(prefix.length) else null })
} else {
val domainEnabledRules = state.domainEnabledRules[domain] ?: emptySet()
val domainDisabledRules = state.domainDisabledRules[domain] ?: emptySet()
enabledRules.addAll( domainEnabledRules.mapNotNull { if (it.startsWith(prefix)) it.substring(prefix.length) else null })
disabledRules.addAll(domainDisabledRules.mapNotNull { if (it.startsWith(prefix)) it.substring(prefix.length) else null })
}
return enabledRules to disabledRules
}
private fun prepareForNoChunkTags(rule: Rule) {
@Suppress("TestOnlyProblems")
fun relaxChunkConditions(token: PatternToken, positive: Boolean) {
@@ -185,7 +199,7 @@ object LangTool : GrazieStateLifecycle {
val inappropriateExamples = inappropriateExamples[rule.id]
if (inappropriateExamples != null) {
return rule.incorrectExamples
.filterNot { example -> inappropriateExamples.any { example.example.contains(it)} }
.filterNot { example -> inappropriateExamples.any { example.example.contains(it) } }
}
return rule.incorrectExamples
}
@@ -202,9 +216,10 @@ object LangTool : GrazieStateLifecycle {
return suffixLength + prefixLength >= distance
}
internal fun isRuleEnabledByDefault(lang: Lang, ruleId: String): Boolean {
val activeIds = rulesEnabledByDefault.computeIfAbsent(lang) {
createTool(lang, GrazieConfig.State()).allActiveRules.map { it.id }.toSet()
internal fun isRuleEnabledByDefault(lang: Lang, ruleId: String, domain: TextStyleDomain): Boolean {
val rules = rulesEnabledByDefault.computeIfAbsent(lang) { ConcurrentHashMap() }
val activeIds = rules.computeIfAbsent(domain) {
createTool(lang, GrazieConfig.State(), domain).allActiveRules.map { it.id }.toSet()
}
return activeIds.contains(ruleId)
}
@@ -214,6 +229,8 @@ object LangTool : GrazieStateLifecycle {
prevState.availableLanguages == newState.availableLanguages
&& prevState.userDisabledRules == newState.userDisabledRules
&& prevState.userEnabledRules == newState.userEnabledRules
&& prevState.domainEnabledRules == newState.domainEnabledRules
&& prevState.domainDisabledRules == newState.domainDisabledRules
) return
langs.clear()
@@ -34,4 +34,9 @@ public class RuleIdeClient implements RuleClient {
public boolean hasLocalMode() {
return true;
}
@Override
public boolean showIdeStyles() {
return true;
}
}
@@ -10,6 +10,7 @@ import com.intellij.grazie.ide.msg.CONFIG_STATE_TOPIC
import com.intellij.grazie.ide.msg.GrazieStateLifecycle
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.jlanguage.LangTool
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ex.ApplicationUtil
import com.intellij.openapi.components.Service
@@ -113,7 +114,7 @@ class GrazieCheckers(coroutineScope: CoroutineScope) : GrazieStateLifecycle {
for (lang in GrazieConfig.get().availableLanguages) {
if (lang.isEnglish()) continue
val tool = LangTool.getTool(lang)
val tool = LangTool.getTool(lang, TextStyleDomain.Other)
tool.allSpellingCheckRules.firstOrNull()
?.let { set.add(SpellerTool(tool, lang, it)) }
}
@@ -58,11 +58,10 @@ record ChangeLanguageVariant(Lang from, Lang to, boolean wasOxford, boolean toOx
languages.add(to);
return s.copy(
languages, s.getEnabledGrammarStrategies(), s.getDisabledGrammarStrategies(),
s.getEnabledCommitIntegration(),
s.getUserDisabledRules(), s.getUserEnabledRules(),
languages, s.getEnabledGrammarStrategies(), s.getDisabledGrammarStrategies(), s.getEnabledCommitIntegration(),
s.getUserDisabledRules(), s.getUserEnabledRules(), s.getDomainDisabledRules(), s.getDomainEnabledRules(),
s.getSuppressingContext(), s.getDetectionContext(), s.getCheckingContext(), s.getVersion(),
s.getStyleProfile(), s.getParameters(), s.getUseOxfordSpelling(), s.getAutoFix()
s.getStyleProfile(), s.getParameters(), s.getParametersPerDomain(), s.getUseOxfordSpelling(), s.getAutoFix()
);
});
if (from.isEnglish()) {
@@ -1,6 +1,8 @@
package com.intellij.grazie.text;
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;
@@ -82,11 +84,21 @@ public abstract class Rule {
/**
* @return whether this rule is enabled by default
*
* @deprecated Use {@link #isEnabledByDefault(TextStyleDomain)} instead.
*/
@Deprecated(forRemoval = true)
public boolean isEnabledByDefault() {
return true;
}
/**
* @return whether this rule is enabled by default in the given domain
*/
public boolean isEnabledByDefault(TextStyleDomain domain) {
return true;
}
/**
* @return an optional navigatable to open from "Rule X settings" quick fix.
*/
@@ -94,14 +106,15 @@ public abstract class Rule {
return null;
}
@SuppressWarnings("unused")
public final boolean isCurrentlyEnabled() {
return isEnabledInState(GrazieConfig.Companion.get());
public final boolean isCurrentlyEnabled(TextContent content) {
GrazieConfig.State state = GrazieConfig.Companion.get();
TextStyleDomain domain = TextUtilsKt.getTextDomain(content);
return isEnabledInState(state, domain);
}
public final boolean isEnabledInState(GrazieConfig.State state) {
return isEnabledByDefault() ? !state.getUserDisabledRules().contains(globalId)
: state.getUserEnabledRules().contains(globalId);
public final boolean isEnabledInState(GrazieConfig.State state, TextStyleDomain domain) {
return isEnabledByDefault(domain) ? !state.isRuleDisabled(globalId, domain)
: state.isRuleEnabled(globalId, domain);
}
@Override
@@ -33,6 +33,7 @@ import com.intellij.grazie.style.TextLevelFix;
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.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
@@ -61,6 +62,7 @@ import java.util.regex.Pattern;
import static com.intellij.grazie.text.GrazieProblem.getQuickFixText;
import static com.intellij.grazie.text.GrazieProblem.visualizeSpace;
import static com.intellij.grazie.utils.TextUtilsKt.getTextDomain;
import static com.intellij.grazie.utils.UtilsKt.ijRange;
@SuppressWarnings("NonAsciiCharacters")
@@ -108,7 +110,7 @@ public final class TreeRuleChecker {
public static final String SMART_APOSTROPHE = "Grazie.RuleEngine.En.Typography.SMART_APOSTROPHE";
public static List<Rule> getRules(Language language) {
if (!StyleConfigurable.Companion.getRuleLanguages().contains(language) || SentenceBatcher.findInstalledLTLanguage(language) == null) {
if (!StyleConfigurable.getRuleEngineLanguages().contains(language) || SentenceBatcher.findInstalledLTLanguage(language) == null) {
return List.of();
}
@@ -161,8 +163,8 @@ public final class TreeRuleChecker {
}
@Override
public boolean isEnabledByDefault() {
return rule.isRuleEnabledByDefault(GrazieConfig.Companion.get().getTextStyle(), RuleIdeClient.INSTANCE);
public boolean isEnabledByDefault(TextStyleDomain domain) {
return rule.isRuleEnabledByDefault(GrazieConfig.Companion.get().getTextStyle(domain), RuleIdeClient.INSTANCE);
}
@SuppressWarnings("SuspiciousMethodCalls")//false negative in Qodana
@@ -170,7 +172,7 @@ public final class TreeRuleChecker {
public Navigatable editSettings() {
RuleSetting setting = new RuleSetting(rule);
LanguageToolkit toolkit = LanguageToolkit.forLanguage(rule.language());
return StyleConfigurable.affectedSettings(toolkit).contains(setting)
return StyleConfigurable.featuredSettings(toolkit).contains(setting)
? StyleConfigurable.focusSetting(setting, null)
: null;
}
@@ -210,7 +212,7 @@ public final class TreeRuleChecker {
try {
Cached cached = ref.get();
if (cached == null || !cached.sentences.equals(sentences)) {
List<ai.grazie.rules.Rule> rules = enabledRules(sentences.getFirst().tree);
List<ai.grazie.rules.Rule> rules = enabledRules(sentences.getFirst().tree, text);
List<MatchingResult> matches = matchTrees(trees, rules);
ref.set(cached = new Cached(sentences, matches));
}
@@ -224,10 +226,10 @@ public final class TreeRuleChecker {
}
}
private static List<ai.grazie.rules.Rule> enabledRules(Tree sampleTree) {
private static List<ai.grazie.rules.Rule> enabledRules(Tree sampleTree, TextContent content) {
Language language = sampleTree.treeSupport().getGrazieLanguage();
LanguageToolkit toolkit = LanguageToolkit.forLanguage(language);
List<ai.grazie.rules.Rule> rules = ContainerUtil.filter(toolkit.publishedRules(), r -> toGrazieRule(r).isCurrentlyEnabled());
List<ai.grazie.rules.Rule> rules = ContainerUtil.filter(toolkit.publishedRules(), r -> toGrazieRule(r).isCurrentlyEnabled(content));
if (sampleTree.isFlat()) {
return ContainerUtil.filter(rules, r -> r.supportsFlatTrees());
}
@@ -249,8 +251,9 @@ public final class TreeRuleChecker {
var parameters = new HashMap<String, String>();
var ltLanguage = sentences.getFirst().tree.language();
Language language = sentences.getFirst().tree.treeSupport().getGrazieLanguage();
TextContent content = sentences.getFirst().extractedText;
LanguageToolkit toolkit = LanguageToolkit.forLanguage(language);
toolkit.allParameters(RuleIdeClient.INSTANCE).forEach(p -> parameters.put(p.id(), getParamValue(p, language)));
toolkit.allParameters(RuleIdeClient.INSTANCE).forEach(p -> parameters.put(p.id(), getParamValue(p, language, content)));
if (language == Language.ENGLISH || language == Language.GERMAN) {
String[] countries = ltLanguage.getCountries();
if (countries.length > 0) {
@@ -264,10 +267,15 @@ public final class TreeRuleChecker {
return new ParameterValues(parameters);
}
private static @Nullable String getParamValue(Parameter param, Language language) {
String value = GrazieConfig.Companion.get().paramValue(language, param);
private static @Nullable String getParamValue(Parameter param, Language language, TextContent content) {
TextStyleDomain domain = getTextDomain(content);
String value = GrazieConfig.Companion.get().paramValue(domain, language, param);
if (value == null || !ContainerUtil.exists(param.possibleValues(RuleIdeClient.INSTANCE), v -> value.equals(v.id()))) {
return param.defaultValue(GrazieConfig.Companion.get().getTextStyle(), RuleIdeClient.INSTANCE).id();
TextStyle textStyle = TextStyle.styles(RuleIdeClient.INSTANCE).stream()
.filter(it -> it.id().equals(domain.name()))
.findFirst()
.orElseGet(() -> GrazieConfig.Companion.get().getTextStyle());
return param.defaultValue(textStyle, RuleIdeClient.INSTANCE).id();
}
return value;
}
@@ -355,8 +363,8 @@ public final class TreeRuleChecker {
Map<Language, List<ai.grazie.rules.Rule>> rules = new LinkedHashMap<>();
for (SentenceWithContent ds : doc) {
Language language = ds.sentence.language;
if (!rules.containsKey(language) && StyleConfigurable.Companion.getRuleLanguages().contains(language)) {
rules.put(language, enabledRules(ds.sentence.treeOrThrow()));
if (!rules.containsKey(language) && StyleConfigurable.getRuleEngineLanguages().contains(language)) {
rules.put(language, enabledRules(ds.sentence.treeOrThrow(), ds.content));
}
}
@@ -2,10 +2,30 @@ package com.intellij.grazie.utils
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.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.openapi.util.TextRange
import com.intellij.util.containers.ContainerUtil.createConcurrentSoftValueMap
import ai.grazie.text.TextRange as GrazieTextRange
private val affectedGlobalRules = createConcurrentSoftValueMap<Language, Set<String>>()
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)
.filterIsInstance<RuleSetting>()
.map { prefix + it.rule.id }
.toHashSet()
}
}
fun getLanguageIfAvailable(text: String): Language? {
return LangDetector.getLanguage(text)?.takeIf { HighlightingUtil.findInstalledLang(it) != null }
}
@@ -1,7 +1,13 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.grazie.utils
import ai.grazie.rules.RuleClient
import ai.grazie.rules.settings.TextStyle
import com.intellij.grazie.text.TextContent
import com.intellij.grazie.text.TextContent.TextDomain
import com.intellij.grazie.utils.TextStyleDomain.*
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vcs.ui.CommitMessage
import java.util.regex.Matcher
import java.util.regex.Pattern
@@ -79,7 +85,8 @@ object Text {
inToken = true
textTokens++
}
} else {
}
else {
inToken = false
if ("(){}[]<>=+-*/%|&!;,.:\\@$#^".contains(c)) {
codeChars++
@@ -138,3 +145,40 @@ object Text {
return TextRange(start, end)
}
}
enum class TextStyleDomain {
Commit, AIPrompt, CodeDocumentation, CodeComment, Other;
val textStyle: TextStyle
get() = when (this) {
Commit -> TextStyle.Commit
CodeComment -> TextStyle.CodeComment
CodeDocumentation -> TextStyle.CodeDocumentation
AIPrompt -> TextStyle.AIPrompt
else -> throw IllegalArgumentException("TextStyle can't be defined by '$this'")
}
}
fun TextStyle.getTextDomain(): TextStyleDomain = TextStyleDomain.entries.find { id == it.name } ?: Other
fun TextContent.getTextDomain(): TextStyleDomain {
val style = when (this.domain) {
TextDomain.COMMENTS -> CodeComment
TextDomain.DOCUMENTATION -> CodeDocumentation
else -> null
}
if (style != null) return style
val file = this.containingFile
if (CommitMessage.isCommitMessage(file)) return Commit
if ("ChatInput" == file.getLanguage().id) return AIPrompt
return Other
}
fun getOtherDomainStyles(): List<TextStyle> {
val client = object : RuleClient {
override fun showIdeStyles(): Boolean {
return false
}
}
return TextStyle.styles(client)
}
@@ -106,10 +106,10 @@ grazie.settings.grammar.cloud.only.rule=This rule is only available in the Cloud
grazie.settings.configurable.name=Processing
grazie.settings.auto.apply.fixes.label=Automatically fix simple issues as you type (e.g., convert hyphens to dashes)
grazie.settings.use.oxford.spelling.checkbox=Use Oxford Spelling for British English
grazie.settings.style.configurable.name=Writing Style
grazie.settings.writing.style.hint=Define how writing is checked in different contexts and languages. Each domain may have its own style and rules.
grazie.settings.writing.style.text=Writing style:
grazie.settings.style.language.chooser.label=Featured rules for:
grazie.settings.style.configure.all.rules.link=Configure all rules
grazie.settings.writing.style.domain=Domain:
grazie.settings.language.chooser.label=Language:
grazie.settings.style.configurable.example.prefix=Example:
grazie.settings.style.configurable.corrected.prefix=Corrected:
grazie.settings.style.configurable.reset.to.default.link=Reset to default
@@ -118,6 +118,8 @@ grazie.settings.style.configurable.collapse.link=Collapse
grazie.settings.style.configurable.expand.examples.link=Show examples
grazie.settings.style.configurable.collapse.examples.link=Hide examples
grazie.settings.style.search.placeholder=Find rules that match keywords or phrases
grazie.settings.style.rules.all=All rules
grazie.settings.style.rules.other=Other rules
# suppress inspection "UnusedProperty"
grazie.settings.style.profile.display.Informal=Casual (messengers, forums)
# suppress inspection "UnusedProperty"
@@ -128,6 +130,16 @@ grazie.settings.style.profile.display.Public=Public (blog posts, documentation)
grazie.settings.style.profile.display.Formal=Formal (official communication)
# suppress inspection "UnusedProperty"
grazie.settings.style.profile.display.Academic=Academic
# suppress inspection "UnusedProperty"
grazie.settings.domain.profile.display.CodeComment=Code comments
# suppress inspection "UnusedProperty"
grazie.settings.domain.profile.display.Commit=Commit messages
# suppress inspection "UnusedProperty"
grazie.settings.domain.profile.display.CodeDocumentation=In-code documentation
# suppress inspection "UnusedProperty"
grazie.settings.domain.profile.display.AIPrompt=AI Chat
# suppress inspection "UnusedProperty"
grazie.settings.domain.profile.display.Other=Other
# Dictionary Variables
grazie.spellcheck.dictionary.name=Extended Dictionary
@@ -2,6 +2,7 @@
package com.intellij.grazie
import ai.grazie.nlp.langs.LanguageISO
import ai.grazie.rules.settings.TextStyle
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.grazie.grammar.LanguageToolChecker
import com.intellij.grazie.ide.inspection.grammar.GrazieInspection
@@ -14,6 +15,8 @@ import com.intellij.grazie.text.TextChecker
import com.intellij.grazie.text.TextContent
import com.intellij.grazie.text.TextExtractor
import com.intellij.grazie.text.TextProblem
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.grazie.utils.TextStyleDomain.*
import com.intellij.grazie.utils.filterFor
import com.intellij.lang.Language
import com.intellij.openapi.Disposable
@@ -129,7 +132,12 @@ abstract class GrazieTestBase : BasePlatformTestCase() {
}
}
protected fun enableProofreadingFor(languages: Set<Lang>) {
@JvmOverloads
protected fun enableProofreadingFor(
languages: Set<Lang>,
domainEnabledRules: MutableMap<TextStyleDomain, Set<String>> = HashMap(),
domainDisabledRules: MutableMap<TextStyleDomain, Set<String>> = HashMap(),
) {
// Load langs manually to prevent potential deadlock
val enabledLanguages = languages + GrazieConfig.get().enabledLanguages
loadLangs(enabledLanguages, project)
@@ -141,10 +149,17 @@ abstract class GrazieTestBase : BasePlatformTestCase() {
isCheckInDocumentationEnabled = true,
enabledLanguages = additionalEnabledContextLanguages.map { it.id }.toSet(),
)
val domains = setOf(Commit, AIPrompt, CodeDocumentation, CodeComment)
if (domainEnabledRules.isEmpty()) {
domains.forEach { domainEnabledRules[it] = enabledRules + additionalEnabledRules }
}
state.copy(
enabledLanguages = enabledLanguages,
userEnabledRules = enabledRules + additionalEnabledRules,
checkingContext = checkingContext
checkingContext = checkingContext,
styleProfile = TextStyle.Unspecified.id,
domainEnabledRules = domainEnabledRules,
domainDisabledRules = domainDisabledRules,
)
}
@@ -1,8 +1,10 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.grazie.ide.language
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.GrazieTestBase
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.utils.TextStyleDomain
import com.intellij.openapi.components.service
import com.intellij.openapi.util.Disposer
import com.intellij.spellchecker.ProjectDictionaryLayer
@@ -19,7 +21,6 @@ import java.util.function.Consumer
class JavaSupportTest : GrazieTestBase() {
override val additionalEnabledRules: Set<String> = setOf("LanguageTool.EN.UPPERCASE_SENTENCE_START", "LanguageTool.EN.FILE_EXTENSIONS_CASE")
override val enableGrazieChecker: Boolean = true
override fun getProjectDescriptor(): LightProjectDescriptor {
@@ -166,6 +167,9 @@ class JavaSupportTest : GrazieTestBase() {
}
fun `test no highlighting after fixing an error within the same range`() {
GrazieConfig.update {
it.withDomainEnabledRules(TextStyleDomain.CodeDocumentation, setOf("LanguageTool.EN.FILE_EXTENSIONS_CASE"))
}
runHighlightTestForFile("ide/language/java/PDF.java")
myFixture.launchAction(myFixture.findSingleIntention("PDF"))
myFixture.checkHighlighting()
@@ -7,7 +7,6 @@ import com.intellij.grazie.jlanguage.Lang
class MarkdownSupportTest : GrazieTestBase() {
override val additionalEnabledRules: Set<String> = setOf(
"LanguageTool.EN.UPPERCASE_SENTENCE_START",
"LanguageTool.EN.COMMA_COMPOUND_SENTENCE",
"LanguageTool.EN.EN_QUOTES"
)
@@ -0,0 +1,31 @@
package com.intellij.grazie.text
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.GrazieTestBase
import com.intellij.grazie.utils.TextStyleDomain
class TextStyleDomainTest: GrazieTestBase() {
fun `test honor domain settings`() {
GrazieConfig.update {
it.copy(styleProfile = "Academic")
.withDomainDisabledRules(TextStyleDomain.CodeComment, setOf("LanguageTool.EN.EN_A_VS_AN"))
.withDomainDisabledRules(TextStyleDomain.CodeDocumentation, setOf("LanguageTool.EN.EN_A_VS_AN"))
}
myFixture.configureByText("C.java", """
/**
* It is an cat of human
*/
class C {
// It is an friend of human
void foo(int x) {}
}
""".trimIndent())
myFixture.checkHighlighting()
myFixture.configureByText(".md", """
It is <GRAMMAR_ERROR descr="EN_A_VS_AN">an</GRAMMAR_ERROR> dog of human
""".trimIndent())
myFixture.checkHighlighting()
}
}
@@ -1,8 +1,10 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.grazie
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.GrazieTestBase
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.utils.TextStyleDomain
class PythonGrazieSupportTest : GrazieTestBase() {
override fun getBasePath() = "python/testData/grazie/"
@@ -27,6 +29,7 @@ class PythonGrazieSupportTest : GrazieTestBase() {
// PY-53047
fun `test docstring tags are excluded`() {
GrazieConfig.update { it.withDomainEnabledRules(TextStyleDomain.CodeDocumentation, enabledRules) }
runHighlightTestForFile("DocstringTagsAreExcluded.py")
}
}