diff --git a/plugins/grazie/resources/META-INF/plugin.xml b/plugins/grazie/resources/META-INF/plugin.xml index 46b01eae330d..160a873ac62a 100644 --- a/plugins/grazie/resources/META-INF/plugin.xml +++ b/plugins/grazie/resources/META-INF/plugin.xml @@ -133,8 +133,6 @@ - - messages.GrazieBundle diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/text/TreeRuleChecker.java b/plugins/grazie/src/main/kotlin/com/intellij/grazie/text/TreeRuleChecker.java index 958234a54825..5c63838c18b2 100644 --- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/text/TreeRuleChecker.java +++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/text/TreeRuleChecker.java @@ -58,8 +58,6 @@ import java.net.URL; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import static com.intellij.grazie.text.GrazieProblem.getQuickFixText; import static com.intellij.grazie.text.GrazieProblem.visualizeSpace; @@ -606,29 +604,4 @@ public final class TreeRuleChecker { return match.rule().shouldSuppressInCodeLikeFragments(); } } - - public static class DocProblemFilter extends ProblemFilter { - private static final Pattern PY_DOC_PARAM = Pattern.compile("[a-z0-9_]+\\s*:\\s+\\p{L}+( or \\p{L}+)*"); - - @Override - public boolean shouldIgnore(@NotNull TextProblem problem) { - TextContent text = problem.getText(); - if (text.getDomain() == TextDomain.DOCUMENTATION) { - List ranges = problem.getHighlightRanges(); - String psiClass = text.getCommonParent().getClass().getName(); - - //todo remove after https://youtrack.jetbrains.com/issue/PY-59061 is fixed - if (psiClass.equals("com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl")) { - Matcher matcher = PY_DOC_PARAM.matcher(text); - while (matcher.find()) { - if (ContainerUtil.exists(ranges, r -> r.intersects(matcher.start(), matcher.end()))) { - return true; - } - } - } - } - - return false; - } - } } diff --git a/python/python-grazie/src/com/intellij/python/grazie/PythonProblemFilter.kt b/python/python-grazie/src/com/intellij/python/grazie/PythonProblemFilter.kt index 966389d8dc67..45956ea6b275 100644 --- a/python/python-grazie/src/com/intellij/python/grazie/PythonProblemFilter.kt +++ b/python/python-grazie/src/com/intellij/python/grazie/PythonProblemFilter.kt @@ -5,6 +5,10 @@ import com.intellij.grazie.text.RuleGroup import com.intellij.grazie.text.TextContent import com.intellij.grazie.text.TextProblem import com.intellij.grazie.utils.ProblemFilterUtil +import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl +import java.util.regex.Pattern + +private val PY_DOC_PARAM = Pattern.compile("[a-z0-9_]+\\s*:\\s+\\p{L}+( or \\p{L}+)*") private class PythonProblemFilter : ProblemFilter() { override fun shouldIgnore(problem: TextProblem): Boolean { @@ -12,6 +16,21 @@ private class PythonProblemFilter : ProblemFilter() { return domain == TextContent.TextDomain.DOCUMENTATION && (ProblemFilterUtil.isUndecoratedSingleSentenceIssue(problem) || ProblemFilterUtil.isInitialCasingIssue(problem) || - problem.fitsGroup(RuleGroup(RuleGroup.UNDECORATED_SENTENCE_SEPARATION))) + problem.fitsGroup(RuleGroup(RuleGroup.UNDECORATED_SENTENCE_SEPARATION)) || + fitsPyDocParam(problem)) + } + + private fun fitsPyDocParam(problem: TextProblem): Boolean { + val text = problem.text + //todo remove after https://youtrack.jetbrains.com/issue/PY-59061 is fixed + if (text.domain == TextContent.TextDomain.DOCUMENTATION && text.getCommonParent().getParent() is PyStringLiteralExpressionImpl) { + val matcher = PY_DOC_PARAM.matcher(text) + while (matcher.find()) { + if (problem.highlightRanges.any { it.intersects(matcher.start(), matcher.end()) }) { + return true + } + } + } + return false } } \ No newline at end of file diff --git a/python/python-grazie/src/com/intellij/python/grazie/PythonTextExtractor.kt b/python/python-grazie/src/com/intellij/python/grazie/PythonTextExtractor.kt index d11f686e9362..65f913a4eaaf 100644 --- a/python/python-grazie/src/com/intellij/python/grazie/PythonTextExtractor.kt +++ b/python/python-grazie/src/com/intellij/python/grazie/PythonTextExtractor.kt @@ -7,49 +7,75 @@ import com.intellij.grazie.text.TextContentBuilder import com.intellij.grazie.text.TextExtractor import com.intellij.grazie.utils.Text import com.intellij.grazie.utils.getNotSoDistantSimilarSiblings +import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement -import com.intellij.psi.impl.source.tree.LeafPsiElement import com.intellij.psi.impl.source.tree.PsiCommentImpl import com.intellij.psi.util.PsiUtilCore +import com.jetbrains.python.PyStringFormatParser +import com.jetbrains.python.PyStringFormatParser.ConstantChunk import com.jetbrains.python.PyTokenTypes -import com.jetbrains.python.PyTokenTypes.FSTRING_TEXT import com.jetbrains.python.documentation.docstrings.SphinxDocString +import com.jetbrains.python.psi.PyBinaryExpression import com.jetbrains.python.psi.PyFormattedStringElement +import com.jetbrains.python.psi.PyStringLiteralExpression +import com.jetbrains.python.psi.impl.PyStringLiteralDecoder import java.util.regex.Pattern +import java.util.regex.Pattern.quote private val KNOWN_DOCSTRING_TAGS_PATTERN = SphinxDocString.ALL_TAGS.joinToString("|", transform = Pattern::quote, prefix = "(", postfix = ")") private val DOCSTRING_DIRECTIVE_PATTERN = "^$KNOWN_DOCSTRING_TAGS_PATTERN[^\n:]*: *".toPattern(Pattern.MULTILINE) internal class PythonTextExtractor : TextExtractor() { - override fun buildTextContent(root: PsiElement, allowedDomains: MutableSet): TextContent? { - val elementType = PsiUtilCore.getElementType(root) - if (elementType in PyTokenTypes.STRING_NODES) { - val domain = if (elementType == PyTokenTypes.DOCSTRING) TextDomain.DOCUMENTATION else TextDomain.LITERALS - if (domain !in allowedDomains) return null - val stringContent = TextContentBuilder.FromPsi.removingIndents(" \t") - .removingLineSuffixes(" \t") - .withUnknown(this::isUnknownFragment) - .build(root.parent, domain) - if (stringContent != null && domain == TextDomain.DOCUMENTATION && TextDomain.DOCUMENTATION in allowedDomains) { - return stringContent.excludeDocstringTags() + override fun buildTextContents(root: PsiElement, allowedDomains: Set): List { + if (root is PyStringLiteralExpression) { + val texts = mutableListOf() + val parent = root.parent + if (parent is PyBinaryExpression && root === parent.leftExpression && parent.operator === PyTokenTypes.PERC) { + PyStringFormatParser.parsePercentFormat(root.stringValue).forEach { chunk -> + if (chunk is ConstantChunk) { + val startIndex = root.valueOffsetToTextOffset(chunk.startIndex) + val endIndex = root.valueOffsetToTextOffset(chunk.endIndex) + TextContentBuilder.FromPsi.build(root, TextDomain.LITERALS, TextRange(startIndex, endIndex))?.let { texts.add(it) } + } + } + return texts } - return stringContent + + root.stringElements.forEach { element -> + val ranges = if (element.isFormatted) (element as PyFormattedStringElement).literalPartRanges else listOf(element.contentRange) + val decoder = PyStringLiteralDecoder(element) + val containsEscapes = element.textContains('\\') + ranges.forEach { range -> + val escapeAwareRanges = if (element.isRaw || !containsEscapes) listOf(range) else decoder.decodeRange(range).map { it.first } + escapeAwareRanges.forEach { escapeAwareRange -> + val domain = getDomain(element) + TextContentBuilder.FromPsi + .removingIndents(" \t") + .removingLineSuffixes(" \t") + .build(element, domain, escapeAwareRange)?.let { text -> + if (domain == TextDomain.DOCUMENTATION) text.excludeDocstringTags()?.let { texts.add(it) } else texts.add(text) + } + } + } + } + return texts } if (root is PsiCommentImpl && TextDomain.COMMENTS in allowedDomains) { val siblings = getNotSoDistantSimilarSiblings(root) { it is PsiCommentImpl } - return TextContent.joinWithWhitespace('\n', siblings.mapNotNull { TextContent.builder().build(it, TextDomain.COMMENTS) }) + val text = TextContent.joinWithWhitespace( + '\n', + siblings.mapNotNull { TextContent.builder().build(it, TextDomain.COMMENTS) } + ) ?: return emptyList() + return listOf(text) } - return null + return emptyList() } - private fun isUnknownFragment(element: PsiElement): Boolean { - if (element.parent is PyFormattedStringElement) { - return element !is LeafPsiElement || element.elementType != FSTRING_TEXT - } - - return false + private fun getDomain(element: PsiElement): TextDomain { + val elementType = PsiUtilCore.getElementType(element) + return if (elementType == PyTokenTypes.DOCSTRING) TextDomain.DOCUMENTATION else TextDomain.LITERALS } } diff --git a/python/src/com/jetbrains/python/spellchecker/PythonSpellcheckerStrategy.java b/python/src/com/jetbrains/python/spellchecker/PythonSpellcheckerStrategy.java index 3e2486ec26d7..2e011799a888 100644 --- a/python/src/com/jetbrains/python/spellchecker/PythonSpellcheckerStrategy.java +++ b/python/src/com/jetbrains/python/spellchecker/PythonSpellcheckerStrategy.java @@ -4,6 +4,7 @@ package com.jetbrains.python.spellchecker; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.registry.Registry; import com.intellij.psi.PsiElement; import com.intellij.spellchecker.inspections.PlainTextSplitter; import com.intellij.spellchecker.inspections.Splitter; @@ -20,6 +21,7 @@ import com.jetbrains.python.psi.PyStringLiteralExpression; import com.jetbrains.python.psi.impl.PyStringLiteralDecoder; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -73,12 +75,17 @@ public final class PythonSpellcheckerStrategy extends SpellcheckingStrategy impl } } + @Override + public boolean useTextLevelSpellchecking() { + return Registry.is("spellchecker.grazie.enabled", false); + } + private final StringLiteralTokenizer myStringLiteralTokenizer = new StringLiteralTokenizer(); private final FormatStringTokenizer myFormatStringTokenizer = new FormatStringTokenizer(); @Override public @NotNull Tokenizer getTokenizer(PsiElement element) { - if (element instanceof PyStringLiteralExpression) { + if (element instanceof PyStringLiteralExpression && !useTextLevelSpellchecking()) { final InjectedLanguageManager injectionManager = InjectedLanguageManager.getInstance(element.getProject()); if (element.getTextLength() >= 2 && injectionManager.getInjectedPsiFiles(element) != null) { return EMPTY_TOKENIZER; diff --git a/python/testData/grazie/FStrings.py b/python/testData/grazie/FStrings.py new file mode 100644 index 000000000000..d767b156753a --- /dev/null +++ b/python/testData/grazie/FStrings.py @@ -0,0 +1,7 @@ +world = "World!" +some_text = "lots of cats" + +fstring1 = f"Hello, ${world} There are $some_text. And here are some correct English words to make the language detector work. And a typpo" + +fstring1 = f"The name which group and some other English text. And a typpo" + diff --git a/python/testData/inspections/spelling/escapesInRawAndNormalGluedStringElements.py b/python/testData/inspections/spelling/escapesInRawAndNormalGluedStringElements.py index 80e059183288..a76de5acc277 100644 --- a/python/testData/inspections/spelling/escapesInRawAndNormalGluedStringElements.py +++ b/python/testData/inspections/spelling/escapesInRawAndNormalGluedStringElements.py @@ -1 +1 @@ -('\ncorrect' r'\ncorrect') \ No newline at end of file +('\ncorrect' r'\ncorrect') \ No newline at end of file diff --git a/python/testData/inspections/spelling/typoAfterEscapeSequence.py b/python/testData/inspections/spelling/typoAfterEscapeSequence.py index 624e0e5a39ba..06fd71cb4745 100644 --- a/python/testData/inspections/spelling/typoAfterEscapeSequence.py +++ b/python/testData/inspections/spelling/typoAfterEscapeSequence.py @@ -1,4 +1,5 @@ ''' \n \t \\ - @xyzzymethod + @xyzzymethod + typopo _fields = %(field_names)r'''% locals() \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/grazie/PythonGrazieSupportTest.kt b/python/testSrc/com/jetbrains/python/grazie/PythonGrazieSupportTest.kt index 7ef5e590290a..660d9537702b 100644 --- a/python/testSrc/com/jetbrains/python/grazie/PythonGrazieSupportTest.kt +++ b/python/testSrc/com/jetbrains/python/grazie/PythonGrazieSupportTest.kt @@ -10,6 +10,10 @@ class PythonGrazieSupportTest : GrazieTestBase() { override fun getBasePath() = "python/testData/grazie/" override fun isCommunity() = true + fun `test f-strings`() { + runHighlightTestForFile("FStrings.py") + } + fun `test grammar check in constructs`() { runHighlightTestForFile("Constructs.py") }