mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[grazie] IJPL-231421 Add a quick fix to report proofreading bugs to YouTrack
Co-authored-by: Peter Gromov <peter.gromov@jetbrains.com> Merge-request: IJ-MR-190085 Merged-by: Ilia Permiashkin <ilia.permiashkin@jetbrains.com> GitOrigin-RevId: 3e05ad86630c12d3b2845b85cec4c0824ab2d5b6
This commit is contained in:
committed by
intellij-monorepo-bot
co-authored by
Peter Gromov
parent
fb009ba68b
commit
d01aa9e583
+138
@@ -0,0 +1,138 @@
|
||||
package com.intellij.grazie.ide.inspection.grammar.quickfix
|
||||
|
||||
import ai.grazie.nlp.langs.Language
|
||||
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
|
||||
import com.intellij.codeInspection.IntentionAndQuickFixAction
|
||||
import com.intellij.codeInspection.ProblemDescriptor
|
||||
import com.intellij.codeInspection.util.IntentionFamilyName
|
||||
import com.intellij.codeInspection.util.IntentionName
|
||||
import com.intellij.grazie.GrazieBundle
|
||||
import com.intellij.grazie.cloud.GrazieCloudConnector.Companion.seemsCloudConnected
|
||||
import com.intellij.grazie.mlec.MlecChecker
|
||||
import com.intellij.grazie.spellcheck.TypoProblem
|
||||
import com.intellij.grazie.text.GrazieProblem
|
||||
import com.intellij.grazie.text.TextProblem
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.ide.plugins.PluginManager
|
||||
import com.intellij.ide.plugins.PluginManagerCore
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Iconable
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.SmartPointerManager
|
||||
import com.intellij.psi.SmartPsiFileRange
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.net.URLEncoder
|
||||
import javax.swing.Icon
|
||||
import kotlin.math.abs
|
||||
|
||||
private const val NEW_ISSUE_URL = "https://youtrack.jetbrains.com/newissue"
|
||||
private val SUPPORTED_LANGUAGES = setOf(Language.ENGLISH, Language.GERMAN)
|
||||
private val TRIM_REGEX = Regex("^(?:\\s*\\n)+|(?:\\s*\\n)+$")
|
||||
private val SUPPORTED_CLASSES = setOf(TypoProblem::class.java, GrazieProblem::class.java)
|
||||
|
||||
internal class GrazieYtReportAction(problem: TextProblem) : IntentionAndQuickFixAction(), Iconable {
|
||||
private val naturalLanguage: Language = problem.rule.language
|
||||
private val programmingLanguages = problem.text.containingFile.viewProvider.languages
|
||||
private val isCloud = seemsCloudConnected()
|
||||
private val message = problem.shortMessage
|
||||
private val clazz: Class<*> = problem.javaClass
|
||||
private val elementRange: SmartPsiFileRange
|
||||
private val textRange: SmartPsiFileRange
|
||||
|
||||
init {
|
||||
val file = problem.text.containingFile
|
||||
val manager = SmartPointerManager.getInstance(file.project)
|
||||
elementRange = manager.createSmartPsiFileRangePointer(file, problem.text.commonParent.textRange)
|
||||
textRange = manager.createSmartPsiFileRangePointer(file, problem.text.rangesInFile.reduce(TextRange::union))
|
||||
}
|
||||
|
||||
// maybe expensive, so invoke only if fix is applied
|
||||
private val title by lazy {
|
||||
GrazieBundle.message(
|
||||
"grazie.report.bug.${if (problem.isSpellingProblem) "spelling" else "grammar"}",
|
||||
words.joinToString(),
|
||||
)
|
||||
}
|
||||
private val words by lazy { problem.highlightRanges.map { it.substring(problem.text.toString()) } }
|
||||
@get:Nls
|
||||
private val problemKind by lazy {
|
||||
if (problem.isSpellingProblem) GrazieBundle.message("grazie.report.bug.spelling.name")
|
||||
else if (problem is MlecChecker.MlecProblem) problem.errorText
|
||||
else problem.rule.globalId
|
||||
}
|
||||
private val suggestions by lazy { problem.suggestions.map { it.presentableText } }
|
||||
|
||||
|
||||
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY
|
||||
override fun getName(): @IntentionName String = familyName
|
||||
override fun getFamilyName(): @IntentionFamilyName String = GrazieBundle.message("grazie.report.bug.action.name")
|
||||
override fun getIcon(flags: Int): Icon = AllIcons.ToolbarDecorator.AddYouTrack
|
||||
override fun applyFix(project: Project, file: PsiFile?, editor: Editor?) {
|
||||
val problemText = extractAndTrimText(editor) ?: return
|
||||
BrowserUtil.browse(generateReportURL(problemText))
|
||||
}
|
||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean =
|
||||
editor != null &&
|
||||
clazz in SUPPORTED_CLASSES &&
|
||||
naturalLanguage in SUPPORTED_LANGUAGES &&
|
||||
programmingLanguages
|
||||
.mapNotNull { PluginManager.getPluginByClass(it.javaClass) }
|
||||
.let { it.isNotEmpty() && it.all { descriptor -> PluginManagerCore.isDevelopedByJetBrains(descriptor)} }
|
||||
|
||||
private fun generateReportURL(problemText: String): String {
|
||||
val description = buildString {
|
||||
appendLine("**File's languages:** ${programmingLanguages.joinToString(", ") { it.displayName }}")
|
||||
appendLine("**Text:**\n```\n$problemText\n```")
|
||||
appendLine("**Highlighted words:** ${words.joinToString(" … ")}")
|
||||
appendLine("**Message:** $message")
|
||||
appendLine("**Suggestions:** ${suggestions.joinToString(", ")}")
|
||||
appendLine("**Problem Kind:** $problemKind")
|
||||
appendLine("**Processing:** ${if (isCloud) "Cloud" else "Local"}")
|
||||
appendLine("**${GrazieBundle.message("grazie.report.bug.additional.information")}:**")
|
||||
}
|
||||
|
||||
val paramsString = buildList {
|
||||
add("project=IJPL")
|
||||
add("summary=${URLEncoder.encode(title, "UTF-8")}")
|
||||
add("c=${URLEncoder.encode("Subsystem Tools. Natural Languages", "UTF-8")}")
|
||||
add("c=${URLEncoder.encode("Affected versions ${ApplicationInfo.getInstance().fullVersion}", "UTF-8")}")
|
||||
add("description=${URLEncoder.encode(description, "UTF-8")}")
|
||||
}
|
||||
|
||||
return buildString {
|
||||
append(NEW_ISSUE_URL)
|
||||
if (paramsString.isNotEmpty()) {
|
||||
append("?")
|
||||
append(paramsString.joinToString("&"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAndTrimText(editor: Editor?): String? = extractText(editor)?.replace(TRIM_REGEX, "")
|
||||
|
||||
private fun extractText(editor: Editor?): String? {
|
||||
val editor = editor ?: return null
|
||||
val document = editor.document
|
||||
val elementRange = elementRange.range?.let(TextRange::create)
|
||||
val textRange = textRange.range?.let(TextRange::create)
|
||||
if (elementRange == null && textRange == null) return null
|
||||
if (elementRange != null && textRange == null) return document.getText(elementRange)
|
||||
if (elementRange == null && textRange != null) return extractWithAdditionalLines(document, textRange)
|
||||
if (abs(elementRange!!.length - textRange!!.length) < 100) {
|
||||
return document.getText(elementRange)
|
||||
}
|
||||
return extractWithAdditionalLines(document, textRange)
|
||||
}
|
||||
|
||||
private fun extractWithAdditionalLines(document: Document, range: TextRange): String {
|
||||
val startLine = (document.getLineNumber(range.startOffset) - 1).coerceAtLeast(0)
|
||||
val endLine = (document.getLineNumber(range.endOffset) + 1).coerceAtMost(document.lineCount - 1)
|
||||
val expandedRange = TextRange(document.getLineStartOffset(startLine), document.getLineEndOffset(endLine))
|
||||
return document.getText(expandedRange)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.intellij.grazie.mlec
|
||||
|
||||
import ai.grazie.gec.model.problem.Problem
|
||||
import ai.grazie.gec.model.problem.ProblemFix
|
||||
import ai.grazie.gec.model.problem.SentenceWithProblems
|
||||
import ai.grazie.nlp.langs.Language
|
||||
@@ -14,6 +15,7 @@ import com.intellij.grazie.text.ExternalTextChecker
|
||||
import com.intellij.grazie.text.GrazieProblem
|
||||
import com.intellij.grazie.text.Rule
|
||||
import com.intellij.grazie.text.RuleGroup
|
||||
import com.intellij.grazie.text.TextContent
|
||||
import com.intellij.grazie.text.TextProblem
|
||||
import com.intellij.grazie.utils.Text
|
||||
import com.intellij.grazie.utils.TextStyleDomain
|
||||
@@ -29,13 +31,9 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import java.util.Locale
|
||||
import kotlin.String
|
||||
|
||||
class MlecChecker : ExternalTextChecker() {
|
||||
private val incompleteSentenceMessages = setOf(
|
||||
"Missing verb", "Missing auxiliary verb", "Missing pronoun", "Incorrect subject-verb form", "Incorrect grammar",
|
||||
"Incorrect verb form", "Incorrect noun form", "Multiple mistakes", "Incorrect verb tense form"
|
||||
)
|
||||
|
||||
override fun getRules(locale: Locale): List<Rule> {
|
||||
val targetLanguage = locale.language ?: return emptyList()
|
||||
val (_, rules) = Constants.mlecRules.entries.find { it.key.locale.language == targetLanguage } ?: return emptyList()
|
||||
@@ -44,53 +42,18 @@ class MlecChecker : ExternalTextChecker() {
|
||||
|
||||
override suspend fun checkExternally(context: ProofreadingContext): Collection<TextProblem> {
|
||||
if (!GrazieCloudConnector.seemsCloudConnected() || !context.shouldCheckGrammarStyle()) return emptyList()
|
||||
|
||||
val rules = Constants.mlecRules[context.language] ?: return emptyList()
|
||||
if (rules.none { it.isCurrentlyEnabled(context.text) }) return emptyList()
|
||||
|
||||
val typos = getProblems(context, MlecServerBatcherHolder::class.java)?.takeIf { it.isNotEmpty() } ?: return emptyList()
|
||||
|
||||
return typos
|
||||
.mapNotNull { typo ->
|
||||
val underline: TextRange = typo.highlighting.underline ?: return@mapNotNull null
|
||||
|
||||
val fixes = typo.fixes.map { it.display() }
|
||||
val errorText = underline.substring(context.text.toString())
|
||||
|
||||
val rule =
|
||||
if (context.language == Language.ENGLISH && typo.info.id.id.endsWith("article.missing")) Constants.enMissingArticle
|
||||
else rules.last()
|
||||
if (!rule.isCurrentlyEnabled(context.text)) return@mapNotNull null
|
||||
|
||||
object : GrazieProblem(typo, rule, context.text) {
|
||||
override fun getDescriptionTemplate(isOnTheFly: Boolean): @InspectionMessage String {
|
||||
val description = super.getDescriptionTemplate(isOnTheFly)
|
||||
if (ApplicationManager.getApplication().isUnitTestMode()) return "${rule.globalId}: $description"
|
||||
return description
|
||||
}
|
||||
|
||||
override fun fitsGroup(group: RuleGroup): Boolean {
|
||||
if (RuleGroup.SENTENCE_START_CASE in group.rules && StringUtil.capitalize(errorText) in fixes) {
|
||||
val prev = context.text.subSequence(0, underline.startOffset).toString().trim()
|
||||
if (prev.isBlank() ||
|
||||
Text.Latin.isEndPunctuation(prev) ||
|
||||
Text.findParagraphRange(context.text, underline).startOffset == underline.startOffset) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (RuleGroup.SENTENCE_END_PUNCTUATION in group.rules && "$errorText." in fixes) {
|
||||
return context.text.subSequence(underline.endOffset, context.text.length).isBlank() ||
|
||||
Text.findParagraphRange(context.text, underline).endOffset == underline.endOffset
|
||||
}
|
||||
|
||||
if (RuleGroup.INCOMPLETE_SENTENCE in group.rules && typo.message in incompleteSentenceMessages) {
|
||||
return true
|
||||
}
|
||||
|
||||
return super.fitsGroup(group)
|
||||
}
|
||||
}
|
||||
MlecProblem(typo, rule, context.text, underline)
|
||||
}
|
||||
.filterNot { isKnownMlecBug(it) }
|
||||
}
|
||||
@@ -110,15 +73,15 @@ class MlecChecker : ExternalTextChecker() {
|
||||
|
||||
@Suppress("NonAsciiCharacters")
|
||||
object Constants {
|
||||
const val MLEC_RULE_PREFIX = "Grazie.MLEC"
|
||||
val enMissingArticle = mlecRule(
|
||||
const val MLEC_RULE_PREFIX: String = "Grazie.MLEC"
|
||||
val enMissingArticle: Rule = mlecRule(
|
||||
"$MLEC_RULE_PREFIX.En.MissingArticle",
|
||||
Language.ENGLISH,
|
||||
presentableName = "Missing article check (ML)",
|
||||
category = "Machine Learning",
|
||||
description = "Missing article detection powered by Grazie machine learning"
|
||||
)
|
||||
val mlecRules = mapOf(
|
||||
val mlecRules: Map<Language, List<Rule>> = mapOf(
|
||||
Language.ENGLISH to listOf(
|
||||
enMissingArticle,
|
||||
mlecRule(
|
||||
@@ -191,4 +154,44 @@ class MlecChecker : ExternalTextChecker() {
|
||||
}
|
||||
}
|
||||
|
||||
class MlecProblem(source: Problem, rule: Rule, text: TextContent, private val underline: TextRange) : GrazieProblem(source, rule, text) {
|
||||
companion object {
|
||||
private val incompleteSentenceMessages = setOf(
|
||||
"Missing verb", "Missing auxiliary verb", "Missing pronoun", "Incorrect subject-verb form", "Incorrect grammar",
|
||||
"Incorrect verb form", "Incorrect noun form", "Multiple mistakes", "Incorrect verb tense form"
|
||||
)
|
||||
}
|
||||
|
||||
override fun getDescriptionTemplate(isOnTheFly: Boolean): @InspectionMessage String {
|
||||
val description = super.getDescriptionTemplate(isOnTheFly)
|
||||
if (ApplicationManager.getApplication().isUnitTestMode()) return "${rule.globalId}: $description"
|
||||
return description
|
||||
}
|
||||
|
||||
override fun fitsGroup(group: RuleGroup): Boolean {
|
||||
val fixes = source.fixes.map { it.display() }
|
||||
val errorText = errorText
|
||||
if (RuleGroup.SENTENCE_START_CASE in group.rules && StringUtil.capitalize(errorText) in fixes) {
|
||||
val prev = text.subSequence(0, underline.startOffset).toString().trim()
|
||||
if (prev.isBlank() ||
|
||||
Text.Latin.isEndPunctuation(prev) ||
|
||||
Text.findParagraphRange(text, underline).startOffset == underline.startOffset) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (RuleGroup.SENTENCE_END_PUNCTUATION in group.rules && "$errorText." in fixes) {
|
||||
return text.subSequence(underline.endOffset, text.length).isBlank() ||
|
||||
Text.findParagraphRange(text, underline).endOffset == underline.endOffset
|
||||
}
|
||||
|
||||
if (RuleGroup.INCOMPLETE_SENTENCE in group.rules && source.message in incompleteSentenceMessages) {
|
||||
return true
|
||||
}
|
||||
|
||||
return super.fitsGroup(group)
|
||||
}
|
||||
|
||||
val errorText: String get() = underline.substring(text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.intellij.codeInspection.ProblemDescriptorBase
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.util.InspectionMessage
|
||||
import com.intellij.grazie.cloud.GrazieCloudConnector.Companion.seemsCloudConnected
|
||||
import com.intellij.grazie.grammar.LanguageToolChecker
|
||||
import com.intellij.grazie.ide.fus.AcceptanceRateTracker
|
||||
import com.intellij.grazie.ide.fus.GrazieFUSCounter
|
||||
import com.intellij.grazie.ide.inspection.grammar.GrazieInspection
|
||||
@@ -21,6 +22,7 @@ import com.intellij.grazie.ide.inspection.grammar.quickfix.GrazieEnableCloudActi
|
||||
import com.intellij.grazie.ide.inspection.grammar.quickfix.GrazieMassApplyAction
|
||||
import com.intellij.grazie.ide.inspection.grammar.quickfix.GrazieReplaceTypoQuickFix
|
||||
import com.intellij.grazie.ide.inspection.grammar.quickfix.GrazieRuleSettingsAction
|
||||
import com.intellij.grazie.ide.inspection.grammar.quickfix.GrazieYtReportAction
|
||||
import com.intellij.grazie.ide.language.LanguageGrammarChecking
|
||||
import com.intellij.grazie.spellcheck.TypoProblem
|
||||
import com.intellij.grazie.text.TextChecker.ProofreadingContext
|
||||
@@ -242,6 +244,7 @@ class CheckerRunner(val text: TextContent) {
|
||||
result.add(GrazieRuleSettingsAction(problem.rule, problem.text.getTextDomain()))
|
||||
}
|
||||
result.add(GrazieMassApplyAction())
|
||||
result.add(GrazieYtReportAction(problem))
|
||||
result.add(GrazieEnableCloudAction())
|
||||
return result.toTypedArray()
|
||||
}
|
||||
|
||||
@@ -241,4 +241,14 @@ grazie.mass.apply.dialog.editor.redo=Redo
|
||||
grazie.mass.apply.text.do=Performing write action\u2026
|
||||
grazie.mass.apply.text.diff.style.name=Style
|
||||
grazie.mass.apply.text.diff.grammar.name=Grammar
|
||||
grazie.mass.apply.text.diff.typo.name=Typo
|
||||
grazie.mass.apply.text.diff.typo.name=Typo
|
||||
|
||||
|
||||
# Report proofreading bugs to YouTrack
|
||||
grazie.report.bug.action.name=Report proofreading issues to YouTrack
|
||||
grazie.report.bug.spelling.name=Typo
|
||||
# suppress inspection "UnusedProperty"
|
||||
grazie.report.bug.spelling=Unknown word(s): ''{0}''
|
||||
# suppress inspection "UnusedProperty"
|
||||
grazie.report.bug.grammar=Wrong proofreading warning: ''{0}''
|
||||
grazie.report.bug.additional.information=Additional information
|
||||
|
||||
Reference in New Issue
Block a user