diff --git a/python/helpers/docstring_formatter.py b/python/helpers/docstring_formatter.py index 0891f2cd90be..aa9242ef7e68 100644 --- a/python/helpers/docstring_formatter.py +++ b/python/helpers/docstring_formatter.py @@ -5,7 +5,6 @@ import re import sys import textwrap -import six from six import text_type, u ENCODING = 'utf-8' @@ -264,51 +263,11 @@ def format_numpy(docstring): return format_rest(transformed) -def format_epytext(docstring): - if six.PY3: - return u('Epydoc is not compatible with Python 3 interpreter') - - import epydoc.markup.epytext - from epydoc.markup import DocstringLinker - from epydoc.markup.epytext import parse_docstring, ParseError, _colorize - - def _add_para(doc, para_token, stack, indent_stack, errors): - """Colorize the given paragraph, and add it to the DOM tree.""" - para = _colorize(doc, para_token, errors) - if para_token.inline: - para.attribs['inline'] = True - stack[-1].children.append(para) - - epydoc.markup.epytext._add_para = _add_para - ParseError.is_fatal = lambda self: False - - errors = [] - - class EmptyLinker(DocstringLinker): - def translate_indexterm(self, indexterm): - return "" - - def translate_identifier_xref(self, identifier, label=None): - return identifier - - docstring = parse_docstring(docstring, errors) - docstring, fields = docstring.split_fields() - html = docstring.to_html(EmptyLinker()) - - if errors and not html: - # It's not possible to recover original stacktraces of the errors - error_lines = '\n'.join(text_type(e) for e in errors) - raise Exception('Error parsing docstring. Probable causes:\n' + error_lines) - - return html - - def format_body(docstring_format, input_body): formatter = { 'rest': format_rest, 'google': format_google, 'numpy': format_numpy, - 'epytext': format_epytext }.get(docstring_format, format_rest) return formatter(input_body) 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 30f5096c46fc..3419148320f8 100644 --- a/python/python-grazie/src/com/intellij/python/grazie/PythonTextExtractor.kt +++ b/python/python-grazie/src/com/intellij/python/grazie/PythonTextExtractor.kt @@ -13,13 +13,11 @@ import com.intellij.psi.impl.source.tree.PsiCommentImpl import com.intellij.psi.util.PsiUtilCore import com.jetbrains.python.PyTokenTypes import com.jetbrains.python.PyTokenTypes.FSTRING_TEXT -import com.jetbrains.python.documentation.docstrings.EpydocString import com.jetbrains.python.documentation.docstrings.SphinxDocString import com.jetbrains.python.psi.PyFormattedStringElement import java.util.regex.Pattern -private val KNOWN_DOCSTRING_TAGS_PATTERN = (SphinxDocString.ALL_TAGS + EpydocString.ALL_TAGS) - .joinToString("|", transform = Pattern::quote, prefix = "(", postfix = ")") +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() { diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 01e2135d9aff..9511aec26e22 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -164,8 +164,8 @@ ANN.await.outside.async.function='await' outside async function ### quick doc generator QDOC.module.path.unknown=(Module path is unknown) -QDOC.epydoc.python2.sdk.not.found=You need a configured Python 2 SDK to render Epydoc docstrings QDOC.local.sdk.not.found=You need a configured local Python SDK to render docstrings. +QDOC.docstring.rendering.is.not.supported.for.python.2=Docstring rendering is not supported for Python 2 SDK. QDOC.assigned.to=Assigned to: QDOC.copied.from=Copied from: QDOC.accessor.kind=Accessor kind: diff --git a/python/python-psi-impl/src/com/jetbrains/python/documentation/docstrings/DocStringTagCompletionContributor.java b/python/python-psi-impl/src/com/jetbrains/python/documentation/docstrings/DocStringTagCompletionContributor.java deleted file mode 100644 index 2067b1a4dfc9..000000000000 --- a/python/python-psi-impl/src/com/jetbrains/python/documentation/docstrings/DocStringTagCompletionContributor.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2000-2014 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.jetbrains.python.documentation.docstrings; - -import com.intellij.codeInsight.completion.*; -import com.intellij.codeInsight.lookup.LookupElementBuilder; -import com.intellij.openapi.project.DumbAware; -import com.intellij.patterns.PsiElementPattern; -import com.intellij.psi.PsiFile; -import com.intellij.util.ProcessingContext; -import com.jetbrains.python.psi.PyDocStringOwner; -import com.jetbrains.python.psi.PyExpressionStatement; -import com.jetbrains.python.psi.PyStringLiteralExpression; -import org.jetbrains.annotations.NotNull; - -import static com.intellij.patterns.PlatformPatterns.psiElement; - - -public final class DocStringTagCompletionContributor extends CompletionContributor implements DumbAware { - public static final PsiElementPattern.Capture DOCSTRING_PATTERN = psiElement(PyStringLiteralExpression.class) - .withParent(psiElement(PyExpressionStatement.class).inside(PyDocStringOwner.class)); - - public DocStringTagCompletionContributor() { - extend(CompletionType.BASIC, psiElement().withParent(DOCSTRING_PATTERN), - new CompletionProvider<>() { - @Override - protected void addCompletions(@NotNull CompletionParameters parameters, - @NotNull ProcessingContext context, - @NotNull CompletionResultSet result) { - final PsiFile file = parameters.getOriginalFile(); - DocStringFormat format = DocStringParser.getConfiguredDocStringFormat(file); - if (format == DocStringFormat.EPYTEXT || format == DocStringFormat.REST) { - int offset = parameters.getOffset(); - final String text = file.getText(); - char prefix = format == DocStringFormat.EPYTEXT ? '@' : ':'; - if (offset > 0) { - offset--; - } - StringBuilder prefixBuilder = new StringBuilder(); - while (offset > 0 && (Character.isLetterOrDigit(text.charAt(offset)) || text.charAt(offset) == prefix)) { - prefixBuilder.insert(0, text.charAt(offset)); - if (text.charAt(offset) == prefix) { - offset--; - break; - } - offset--; - } - while (offset > 0) { - offset--; - if (text.charAt(offset) == '\n' || text.charAt(offset) == '\"' || text.charAt(offset) == '\'') { - break; - } - if (!Character.isWhitespace(text.charAt(offset))) { - return; - } - } - String[] allTags = format == DocStringFormat.EPYTEXT ? EpydocString.ALL_TAGS : SphinxDocString.ALL_TAGS; - if (prefixBuilder.length() > 0) { - result = result.withPrefixMatcher(prefixBuilder.toString()); - } - for (String tag : allTags) { - result.addElement(LookupElementBuilder.create(tag)); - } - } - } - }); - } -} diff --git a/python/python-psi-impl/src/com/jetbrains/python/documentation/docstrings/DocStringTagCompletionContributor.kt b/python/python-psi-impl/src/com/jetbrains/python/documentation/docstrings/DocStringTagCompletionContributor.kt new file mode 100644 index 000000000000..d9dd35396bd4 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/documentation/docstrings/DocStringTagCompletionContributor.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.documentation.docstrings + +import com.intellij.codeInsight.completion.* +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.project.DumbAware +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.PsiElementPattern +import com.intellij.util.ProcessingContext +import com.jetbrains.python.psi.PyDocStringOwner +import com.jetbrains.python.psi.PyExpressionStatement +import com.jetbrains.python.psi.PyStringLiteralExpression + +class DocStringTagCompletionContributor : CompletionContributor(), DumbAware { + init { + extend(CompletionType.BASIC, PlatformPatterns.psiElement().withParent(DOCSTRING_PATTERN), + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet, + ) { + val file = parameters.originalFile + if (DocStringParser.getConfiguredDocStringFormat(file) == DocStringFormat.REST) { + var offset = parameters.offset + val text = file.getText() + val prefix = ':' + if (offset > 0) { + offset-- + } + val prefixBuilder = StringBuilder() + while (offset > 0 && (Character.isLetterOrDigit(text[offset]) || text[offset] == prefix)) { + prefixBuilder.insert(0, text[offset]) + if (text[offset] == prefix) { + offset-- + break + } + offset-- + } + while (offset > 0) { + offset-- + if (text[offset] == '\n' || text[offset] == '\"' || text[offset] == '\'') { + break + } + if (!Character.isWhitespace(text[offset])) { + return + } + } + var resultSet = result + if (!prefixBuilder.isEmpty()) { + resultSet = resultSet.withPrefixMatcher(prefixBuilder.toString()) + } + for (tag in SphinxDocString.ALL_TAGS) { + resultSet.addElement(LookupElementBuilder.create(tag)) + } + } + } + }) + } + + companion object { + @JvmField + val DOCSTRING_PATTERN: PsiElementPattern.Capture = + PlatformPatterns.psiElement(PyStringLiteralExpression::class.java) + .withParent( + PlatformPatterns + .psiElement(PyExpressionStatement::class.java) + .inside(PyDocStringOwner::class.java)) + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/DocStringAnnotator.java b/python/python-psi-impl/src/com/jetbrains/python/validation/DocStringAnnotator.java deleted file mode 100644 index c30da5f7f554..000000000000 --- a/python/python-psi-impl/src/com/jetbrains/python/validation/DocStringAnnotator.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2000-2014 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.jetbrains.python.validation; - -import com.intellij.lang.annotation.HighlightSeverity; -import com.intellij.openapi.util.TextRange; -import com.jetbrains.python.PyNames; -import com.jetbrains.python.documentation.docstrings.*; -import com.jetbrains.python.highlighting.PyHighlighter; -import com.jetbrains.python.psi.*; -import org.jetbrains.annotations.NotNull; - -/** - * Highlights doc strings in classes, functions, and files. - */ -public final class DocStringAnnotator extends PyAnnotator { - - @Override - public void visitPyFile(final @NotNull PyFile node) { - annotateDocStringStmt(DocStringUtil.findDocStringExpression(node)); - } - - @Override - public void visitPyFunction(final @NotNull PyFunction node) { - annotateDocStringStmt(DocStringUtil.findDocStringExpression(node.getStatementList())); - } - - @Override - public void visitPyClass(final @NotNull PyClass node) { - annotateDocStringStmt(DocStringUtil.findDocStringExpression(node.getStatementList())); - } - - @Override - public void visitPyAssignmentStatement(@NotNull PyAssignmentStatement node) { - if (node.isAssignmentTo(PyNames.DOC)) { - PyExpression right = node.getAssignedValue(); - if (right instanceof PyStringLiteralExpression) { - getHolder().newSilentAnnotation(HighlightSeverity.INFORMATION).range(right).textAttributes(PyHighlighter.PY_DOC_COMMENT).create(); - annotateDocStringStmt((PyStringLiteralExpression)right); - } - } - } - - @Override - public void visitPyExpressionStatement(@NotNull PyExpressionStatement node) { - if (node.getExpression() instanceof PyStringLiteralExpression && - DocStringUtil.isVariableDocString((PyStringLiteralExpression)node.getExpression())) { - annotateDocStringStmt((PyStringLiteralExpression)node.getExpression()); - } - } - - private void annotateDocStringStmt(final PyStringLiteralExpression stmt) { - if (stmt != null) { - final DocStringFormat format = DocStringParser.getConfiguredDocStringFormat(stmt); - final String[] tags; - if (format == DocStringFormat.EPYTEXT) { - tags = EpydocString.ALL_TAGS; - } - else if (format == DocStringFormat.REST) { - tags = SphinxDocString.ALL_TAGS; - } - else { - return; - } - int pos = 0; - while (true) { - TextRange textRange = DocStringReferenceProvider.findNextTag(stmt.getText(), pos, tags); - if (textRange == null) break; - getHolder().newSilentAnnotation( - HighlightSeverity.INFORMATION).range(textRange.shiftRight(stmt.getTextRange().getStartOffset())).textAttributes(PyHighlighter.PY_DOC_COMMENT_TAG).create(); - pos = textRange.getEndOffset(); - } - } - } -} diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/DocStringAnnotator.kt b/python/python-psi-impl/src/com/jetbrains/python/validation/DocStringAnnotator.kt new file mode 100644 index 000000000000..57503aeeb00f --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/validation/DocStringAnnotator.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.validation + +import com.intellij.lang.annotation.HighlightSeverity +import com.jetbrains.python.PyNames +import com.jetbrains.python.documentation.docstrings.* +import com.jetbrains.python.highlighting.PyHighlighter +import com.jetbrains.python.psi.* + +/** + * Highlights doc strings in classes, functions, and files. + */ +class DocStringAnnotator : PyAnnotator() { + override fun visitPyFile(node: PyFile) { + annotateDocStringStmt(DocStringUtil.findDocStringExpression(node)) + } + + override fun visitPyFunction(node: PyFunction) { + annotateDocStringStmt(DocStringUtil.findDocStringExpression(node.statementList)) + } + + override fun visitPyClass(node: PyClass) { + annotateDocStringStmt(DocStringUtil.findDocStringExpression(node.statementList)) + } + + override fun visitPyAssignmentStatement(node: PyAssignmentStatement) { + if (node.isAssignmentTo(PyNames.DOC)) { + val right = node.assignedValue + if (right is PyStringLiteralExpression) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION).range(right).textAttributes(PyHighlighter.PY_DOC_COMMENT).create() + annotateDocStringStmt(right) + } + } + } + + override fun visitPyExpressionStatement(node: PyExpressionStatement) { + if (node.expression is PyStringLiteralExpression && + DocStringUtil.isVariableDocString(node.expression as PyStringLiteralExpression) + ) { + annotateDocStringStmt(node.expression as PyStringLiteralExpression) + } + } + + private fun annotateDocStringStmt(stmt: PyStringLiteralExpression?) { + if (stmt == null) return + if (DocStringParser.getConfiguredDocStringFormat(stmt) == DocStringFormat.REST) { + val tags = SphinxDocString.ALL_TAGS + var pos = 0 + while (true) { + val textRange = DocStringReferenceProvider.findNextTag(stmt.getText(), pos, tags) + if (textRange == null) break + holder.newSilentAnnotation( + HighlightSeverity.INFORMATION).range(textRange.shiftRight(stmt.getTextRange().getStartOffset())) + .textAttributes(PyHighlighter.PY_DOC_COMMENT_TAG).create() + pos = textRange.endOffset + } + } + } +} diff --git a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/DocStringFormat.java b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/DocStringFormat.java index de9d612d5423..dd0385c0bfdb 100644 --- a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/DocStringFormat.java +++ b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/DocStringFormat.java @@ -15,7 +15,6 @@ */ package com.jetbrains.python.documentation.docstrings; -import com.intellij.psi.PsiElement; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; @@ -25,11 +24,7 @@ import java.util.List; public enum DocStringFormat { - /** - * @see DocStringUtil#ensureNotPlainDocstringFormat(PsiElement) - */ PLAIN("Plain", ""), - EPYTEXT("Epytext", "epytext"), REST("reStructuredText", "rest"), NUMPY("NumPy", "numpy"), GOOGLE("Google", "google"); diff --git a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/DocStringParser.java b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/DocStringParser.java index 2214d46e707f..0fbb10adf62a 100644 --- a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/DocStringParser.java +++ b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/DocStringParser.java @@ -30,7 +30,6 @@ public final class DocStringParser { public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull Substring content) { return switch (format) { case REST -> new SphinxDocString(content); - case EPYTEXT -> new EpydocString(content); case GOOGLE -> new GoogleCodeStyleDocString(content); case NUMPY -> new NumpyDocString(content); case PLAIN -> new PlainDocString(content); @@ -50,9 +49,6 @@ public final class DocStringParser { */ @NotNull public static DocStringFormat guessDocStringFormat(@NotNull String text) { - if (isLikeEpydocDocString(text)) { - return DocStringFormat.EPYTEXT; - } if (isLikeSphinxDocString(text)) { return DocStringFormat.REST; } @@ -107,15 +103,6 @@ public final class DocStringParser { text.contains(":var") || text.contains(":ivar") || text.contains(":cvar"); } - public static boolean isLikeEpydocDocString(@NotNull String text) { - return text.contains("@param ") || - text.contains("@kwarg ") || text.contains("@keyword ") || text.contains("@kwparam ") || - text.contains("@raise ") || text.contains("@raises ") || text.contains("@except ") || text.contains("@exception ") || - text.contains("@return:") || - text.contains("@rtype") || text.contains("@type") || - text.contains("@var") || text.contains("@ivar") || text.contains("@cvar"); - } - public static boolean isLikeGoogleDocString(@NotNull String text) { for (@NonNls String title : StringUtil.findMatches(text, GoogleCodeStyleDocString.SECTION_HEADER, 1)) { if (SectionBasedDocString.isValidSectionTitle(title)) { diff --git a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/EpydocString.java b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/EpydocString.java deleted file mode 100644 index e1c929fddd46..000000000000 --- a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/EpydocString.java +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright 2000-2018 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.jetbrains.python.documentation.docstrings; - -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.xml.util.XmlStringUtil; -import com.jetbrains.python.toolbox.Substring; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - - -public class EpydocString extends TagBasedDocString { - - public static String[] RTYPE_TAGS = new String[] { "rtype", "returntype" }; - public static String[] KEYWORD_ARGUMENT_TAGS = new String[] { "keyword", "kwarg", "kwparam" }; - - public static String[] ALL_TAGS = new String[] { - "@param", "@type", "@return", "@rtype", "@keyword", "@raise", "@ivar", "@cvar", "@var", "@group", "@sort", "@note", "@attention", - "@bug", "@warning", "@version", "@todo", "@deprecated", "@since", "@status", "@change", "@permission", "@requires", - "@precondition", "@postcondition", "@invariant", "@author", "@organization", "@copyright", "@license", "@contact", "@summary", "@see" - }; - - - public static String[] ADDITIONAL = new String[] { - "group", "sort", "note", "attention", - "bug", "warning", "version", "todo", "deprecated", "since", "status", "change", "permission", "requires", - "precondition", "postcondition", "invariant", "author", "organization", "copyright", "license", "contact", "summary", "see" - }; - - public EpydocString(@NotNull Substring docstringText) { - super(docstringText, "@"); - } - - @NotNull - @Override - public String getDescription() { - final String html = inlineMarkupToHTML(myDescription); - assert html != null; - return html; - } - - @NotNull - @Override - public List getKeywordArguments() { - return toUniqueStrings(getKeywordArgumentSubstrings()); - } - - @Override - @Nullable - public String getReturnType() { - return removeInlineMarkup(getReturnTypeSubstring()); - } - - @Override - public String getReturnDescription() { - return inlineMarkupToHTML(getTagValue(RETURN_TAGS)); - } - - @Override - @Nullable - public String getParamType(@Nullable String paramName) { - return removeInlineMarkup(getParamTypeSubstring(paramName)); - } - - @Override - @Nullable - public String getParamDescription(@Nullable String paramName) { - if (paramName == null) { - return null; - } - Substring value = getTagValue(PARAM_TAGS, paramName); - if (value == null) { - value = getTagValue(PARAM_TAGS, "*" + paramName); - } - if (value == null) { - value = getTagValue(PARAM_TAGS, "**" + paramName); - } - return inlineMarkupToHTML(value); - } - - @Nullable - @Override - public String getKeywordArgumentDescription(@Nullable String paramName) { - if (paramName == null) { - return null; - } - return inlineMarkupToHTML(getTagValue(KEYWORD_ARGUMENT_TAGS, paramName)); - } - - @NotNull - @Override - public List getRaisedExceptions() { - return toUniqueStrings(getTagArguments(RAISES_TAGS)); - } - - @Override - public String getRaisedExceptionDescription(@Nullable String exceptionName) { - if (exceptionName == null) { - return null; - } - return removeInlineMarkup(getTagValue(RAISES_TAGS, exceptionName)); - } - - @Override - public String getAttributeDescription() { - final Substring value = getTagValue(VARIABLE_TAGS); - return convertInlineMarkup(value != null ? value.toString() : null, true); - } - - @Nullable - public static String removeInlineMarkup(@Nullable String s) { - return convertInlineMarkup(s, false); - } - - @Nullable - private static String removeInlineMarkup(@Nullable Substring s) { - return convertInlineMarkup(s != null ? s.concatTrimmedLines(" ") : null, false); - } - - @Nullable - private static String convertInlineMarkup(@Nullable String s, boolean toHTML) { - if (s == null) return null; - MarkupConverter converter = toHTML ? new HTMLConverter() : new MarkupConverter(); - converter.appendWithMarkup(s); - return converter.result(); - } - - private static class MarkupConverter { - protected final StringBuilder myResult = new StringBuilder(); - - public void appendWithMarkup(String s) { - int pos = 0; - while(true) { - int bracePos = s.indexOf('{', pos); - if (bracePos < 1) break; - char prevChar = s.charAt(bracePos-1); - if (prevChar >= 'A' && prevChar <= 'Z') { - appendText(s.substring(pos, bracePos - 1)); - int rbracePos = findMatchingEndBrace(s, bracePos); - if (rbracePos < 0) { - pos = bracePos + 1; - break; - } - final String inlineMarkupContent = s.substring(bracePos + 1, rbracePos); - appendMarkup(prevChar, inlineMarkupContent); - pos = rbracePos + 1; - } - else { - appendText(s.substring(pos, bracePos + 1)); - pos = bracePos+1; - } - } - appendText(s.substring(pos)); - } - - protected void appendText(String text) { - myResult.append(text); - } - - protected void appendMarkup(char markupChar, @NotNull String markupContent) { - appendWithMarkup(markupContent); - } - - public String result() { - return myResult.toString(); - } - } - - private static class HTMLConverter extends MarkupConverter { - @Override - protected void appendText(String text) { - myResult.append(joinLines(XmlStringUtil.escapeString(text, false), true)); - } - - @Override - protected void appendMarkup(char markupChar, @NotNull String markupContent) { - if (markupChar == 'U') { - appendLink(markupContent); - return; - } - switch (markupChar) { - case 'I' -> appendTagPair(markupContent, "i"); - case 'B' -> appendTagPair(markupContent, "b"); - case 'C' -> appendTagPair(markupContent, "code"); - default -> myResult.append(StringUtil.escapeXmlEntities(markupContent)); - } - } - - private void appendTagPair(String markupContent, final String tagName) { - myResult.append("<").append(tagName).append(">"); - appendWithMarkup(markupContent); - myResult.append(""); - } - - private void appendLink(@NotNull String markupContent) { - String linkText = StringUtil.escapeXmlEntities(markupContent); - String linkUrl = linkText; - int pos = markupContent.indexOf('<'); - if (pos >= 0 && markupContent.endsWith(">")) { - linkText = StringUtil.escapeXmlEntities(markupContent.substring(0, pos).trim()); - linkUrl = joinLines(StringUtil.escapeXmlEntities(markupContent.substring(pos + 1, markupContent.length() - 1)), false); - } - myResult.append("").append(linkText).append(""); - } - - } - - private static int findMatchingEndBrace(String s, int bracePos) { - int braceCount = 1; - for(int pos=bracePos+1; pos < s.length(); pos++) { - char c = s.charAt(pos); - if (c == '{') braceCount++; - else if (c == '}') { - braceCount--; - if (braceCount == 0) return pos; - } - } - return -1; - } - - private static String joinLines(String s, boolean addSpace) { - while(true) { - int lineBreakStart = s.indexOf('\n'); - if (lineBreakStart < 0) break; - int lineBreakEnd = lineBreakStart+1; - int blankLines = 0; - while(lineBreakEnd < s.length() && (s.charAt(lineBreakEnd) == ' ' || s.charAt(lineBreakEnd) == '\n')) { - if (s.charAt(lineBreakEnd) == '\n') blankLines++; - lineBreakEnd++; - } - if (addSpace) { - String separator = blankLines > 0 ? "

" : " "; - s = s.substring(0, lineBreakStart) + separator + s.substring(lineBreakEnd); - } - else { - s = s.substring(0, lineBreakStart) + s.substring(lineBreakEnd); - } - } - return s; - } - - @Nullable - public static String inlineMarkupToHTML(@Nullable String s) { - return convertInlineMarkup(s, true); - } - - @Nullable - private static String inlineMarkupToHTML(@Nullable Substring s) { - return s != null ? inlineMarkupToHTML(s.concatTrimmedLines(" ")) : null; - } - - @Override - public List getAdditionalTags() { - List list = new ArrayList<>(); - for (String tagName : ADDITIONAL) { - final Map map = myArgTagValues.get(tagName); - if (map != null) { - list.add(tagName); - } - } - return list; - } - - @NotNull - @Override - public List getKeywordArgumentSubstrings() { - return getTagArguments(KEYWORD_ARGUMENT_TAGS); - } - - @Override - public Substring getReturnTypeSubstring() { - return getTagValue(RTYPE_TAGS); - } - - @Override - public Substring getParamTypeSubstring(@Nullable String paramName) { - return paramName == null ? getTagValue("type") : getTagValue("type", paramName); - } - - @Nullable - @Override - public String getAttributeDescription(@Nullable String attrName) { - return attrName != null ? inlineMarkupToHTML(getTagValue(VARIABLE_TAGS, attrName)) : null; - } -} diff --git a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/PyDocstringGenerator.java b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/PyDocstringGenerator.java index b7ca1111d51b..72072ac29450 100644 --- a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/PyDocstringGenerator.java +++ b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/PyDocstringGenerator.java @@ -32,7 +32,9 @@ import com.jetbrains.python.ast.impl.PyUtilCore; import com.jetbrains.python.codeInsight.PyCodeInsightSettings; import com.jetbrains.python.debugger.PySignature; import com.jetbrains.python.debugger.PySignatureCacheManager; -import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.PyAstElementGenerator; +import com.jetbrains.python.psi.PyIndentUtil; +import com.jetbrains.python.psi.StructuredDocString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -86,7 +88,9 @@ public final class PyDocstringGenerator { * generate properly formatted docstring. */ @NotNull - public static PyDocstringGenerator create(@NotNull DocStringFormat format, @NotNull String indentation, @NotNull PsiElement settingsAnchor) { + public static PyDocstringGenerator create(@NotNull DocStringFormat format, + @NotNull String indentation, + @NotNull PsiElement settingsAnchor) { return new PyDocstringGenerator(null, null, format, indentation, settingsAnchor); } @@ -334,8 +338,8 @@ public final class PyDocstringGenerator { @NotNull private String createDocString() { DocStringBuilder builder = null; - if (myDocStringFormat == DocStringFormat.EPYTEXT || myDocStringFormat == DocStringFormat.REST) { - builder = new TagBasedDocStringBuilder(myDocStringFormat == DocStringFormat.EPYTEXT ? "@" : ":"); + if (myDocStringFormat == DocStringFormat.REST) { + builder = new TagBasedDocStringBuilder(SphinxDocString.TAG_PREFIX); TagBasedDocStringBuilder tagBuilder = (TagBasedDocStringBuilder)builder; if (myAddFirstEmptyLine) { tagBuilder.addEmptyLine(); @@ -400,10 +404,9 @@ public final class PyDocstringGenerator { @NotNull private String updateDocString() { DocStringUpdater updater = null; - if (myDocStringFormat == DocStringFormat.EPYTEXT || myDocStringFormat == DocStringFormat.REST) { - final String prefix = myDocStringFormat == DocStringFormat.EPYTEXT ? "@" : ":"; + if (myDocStringFormat == DocStringFormat.REST) { // noinspection ConstantConditions - updater = new TagBasedDocStringUpdater((TagBasedDocString)getStructuredDocString(), prefix, myDocStringIndent); + updater = new TagBasedDocStringUpdater((TagBasedDocString)getStructuredDocString(), SphinxDocString.TAG_PREFIX, myDocStringIndent); } else if (myDocStringFormat == DocStringFormat.GOOGLE) { //noinspection ConstantConditions @@ -416,7 +419,7 @@ public final class PyDocstringGenerator { updater = new NumpyDocStringUpdater((SectionBasedDocString)getStructuredDocString(), myDocStringIndent); } // plain docstring - do nothing - else if (myDocStringText != null){ + else if (myDocStringText != null) { return myDocStringText; } if (updater != null) { @@ -503,6 +506,7 @@ public final class PyDocstringGenerator { private final String myName; private final String myType; private final boolean myReturnValue; + private DocstringParam(@NotNull String name, @Nullable String type, boolean isReturn) { myName = name; myType = type; @@ -551,13 +555,14 @@ public final class PyDocstringGenerator { ", myReturnValue=" + myReturnValue + '}'; } - } + private static class RaiseVisitor extends PyAstRecursiveElementVisitor { private boolean myHasRaise = false; private boolean myHasReturn = false; @Nullable private PyAstExpression myRaiseTarget = null; + @Override public void visitPyRaiseStatement(@NotNull PyAstRaiseStatement node) { myHasRaise = true; @@ -586,7 +591,6 @@ public final class PyDocstringGenerator { } return ""; } - } @Nullable diff --git a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/SphinxDocString.java b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/SphinxDocString.java deleted file mode 100644 index 4252ff75292f..000000000000 --- a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/SphinxDocString.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2000-2014 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.jetbrains.python.documentation.docstrings; - -import com.jetbrains.python.toolbox.Substring; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Collections; -import java.util.List; - - -public class SphinxDocString extends TagBasedDocString { - public static String[] KEYWORD_ARGUMENT_TAGS = new String[] { "keyword", "key" }; - public static String[] ALL_TAGS = new String[] { ":param", ":parameter", ":arg", ":argument", ":keyword", ":key", - ":type", ":raise", ":raises", ":var", ":cvar", ":ivar", - ":return", ":returns", ":rtype", ":except", ":exception" }; - - public SphinxDocString(@NotNull final Substring docstringText) { - super(docstringText, ":"); - } - - @Nullable - protected static String concatTrimmedLines(@Nullable Substring s) { - return s != null ? s.concatTrimmedLines(" ") : null; - } - - @NotNull - @Override - public List getKeywordArguments() { - return toUniqueStrings(getKeywordArgumentSubstrings()); - } - - @Nullable - @Override - public String getKeywordArgumentDescription(@Nullable String paramName) { - if (paramName == null) { - return null; - } - return concatTrimmedLines(getTagValue(KEYWORD_ARGUMENT_TAGS, paramName)); - } - - @Override - public String getReturnType() { - return concatTrimmedLines(getReturnTypeSubstring()); - } - - @Override - public String getParamType(@Nullable String paramName) { - return concatTrimmedLines(getParamTypeSubstring(paramName)); - } - - @Nullable - @Override - public String getParamDescription(@Nullable String paramName) { - return paramName != null ? concatTrimmedLines(getTagValue(PARAM_TAGS, paramName)) : null; - } - - @Override - public String getReturnDescription() { - return concatTrimmedLines(getTagValue(RETURN_TAGS)); - } - - @NotNull - @Override - public List getRaisedExceptions() { - return toUniqueStrings(getTagArguments(RAISES_TAGS)); - } - - @Nullable - @Override - public String getRaisedExceptionDescription(@Nullable String exceptionName) { - if (exceptionName == null) { - return null; - } - return concatTrimmedLines(getTagValue(RAISES_TAGS, exceptionName)); - } - - @Override - public String getAttributeDescription() { - return concatTrimmedLines(getTagValue(VARIABLE_TAGS)); - } - - @Override - public List getAdditionalTags() { - return Collections.emptyList(); - } - - @NotNull - @Override - public List getKeywordArgumentSubstrings() { - return getTagArguments(KEYWORD_ARGUMENT_TAGS); - } - - @Override - public Substring getReturnTypeSubstring() { - return getTagValue("rtype"); - } - - @Override - public Substring getParamTypeSubstring(@Nullable String paramName) { - return paramName == null ? getTagValue("type") : getTagValue("type", paramName); - } - - @NotNull - @Override - public String getDescription() { - return myDescription.replaceAll("\n", "
"); - } - - @Nullable - @Override - public String getAttributeDescription(@Nullable String attrName) { - return attrName != null ? concatTrimmedLines(getTagValue(VARIABLE_TAGS, attrName)) : null; - } -} diff --git a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/SphinxDocString.kt b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/SphinxDocString.kt new file mode 100644 index 000000000000..e12ab6594ab4 --- /dev/null +++ b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/SphinxDocString.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.documentation.docstrings + +import com.jetbrains.python.toolbox.Substring + +class SphinxDocString(docstringText: Substring) : TagBasedDocString(docstringText, TAG_PREFIX) { + override fun getKeywordArguments(): MutableList { + return toUniqueStrings(keywordArgumentSubstrings) + } + + override fun getKeywordArgumentDescription(paramName: String?): String? { + if (paramName == null) { + return null + } + return concatTrimmedLines(getTagValue(KEYWORD_ARGUMENT_TAGS, paramName)) + } + + override fun getReturnType(): String? { + return concatTrimmedLines(returnTypeSubstring) + } + + override fun getParamType(paramName: String?): String? { + return concatTrimmedLines(getParamTypeSubstring(paramName)) + } + + override fun getParamDescription(paramName: String?): String? { + return if (paramName != null) concatTrimmedLines(getTagValue(PARAM_TAGS, paramName)) else null + } + + override fun getReturnDescription(): String? { + return concatTrimmedLines(getTagValue(*RETURN_TAGS)) + } + + override fun getRaisedExceptions(): MutableList { + return toUniqueStrings(getTagArguments(*RAISES_TAGS)) + } + + override fun getRaisedExceptionDescription(exceptionName: String?): String? { + if (exceptionName == null) { + return null + } + return concatTrimmedLines(getTagValue(RAISES_TAGS, exceptionName)) + } + + override fun getAttributeDescription(): String? { + return concatTrimmedLines(getTagValue(*VARIABLE_TAGS)) + } + + override fun getKeywordArgumentSubstrings(): MutableList { + return getTagArguments(*KEYWORD_ARGUMENT_TAGS) + } + + override fun getReturnTypeSubstring(): Substring? { + return getTagValue("rtype") + } + + override fun getParamTypeSubstring(paramName: String?): Substring? { + return if (paramName == null) getTagValue("type") else getTagValue("type", paramName) + } + + override fun getDescription(): String { + return myDescription.replace("\n".toRegex(), "
") + } + + override fun getAttributeDescription(attrName: String?): String? { + return if (attrName != null) concatTrimmedLines(getTagValue(VARIABLE_TAGS, attrName)) else null + } + + companion object { + val KEYWORD_ARGUMENT_TAGS: Array = arrayOf("keyword", "key") + @JvmField + val ALL_TAGS: Array = arrayOf(":param", ":parameter", ":arg", ":argument", ":keyword", ":key", + ":type", ":raise", ":raises", ":var", ":cvar", ":ivar", + ":return", ":returns", ":rtype", ":except", ":exception") + const val TAG_PREFIX: String = ":" + + private fun concatTrimmedLines(s: Substring?): String? { + return s?.concatTrimmedLines(" ") + } + } +} diff --git a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/TagBasedDocString.java b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/TagBasedDocString.java index f7b3c51ca05c..18003b121320 100644 --- a/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/TagBasedDocString.java +++ b/python/python-syntax-core/src/com/jetbrains/python/documentation/docstrings/TagBasedDocString.java @@ -40,16 +40,16 @@ public abstract class TagBasedDocString extends DocStringLineParser implements S private static final Pattern RE_LOOSE_TAG_LINE = Pattern.compile("([a-z]+)\\s+([a-zA-Z_0-9]*)\\s*:?\\s*?([^:]*)"); private static final Pattern RE_ARG_TYPE = Pattern.compile("(.*?)\\s+([a-zA-Z_0-9]+)"); - public static String[] PARAM_TAGS = new String[]{"param", "parameter", "arg", "argument"}; - public static String[] PARAM_TYPE_TAGS = new String[]{"type"}; - public static String[] VARIABLE_TAGS = new String[]{"ivar", "cvar", "var"}; + public static final String[] PARAM_TAGS = new String[]{"param", "parameter", "arg", "argument"}; + public static final String[] PARAM_TYPE_TAGS = new String[]{"type"}; + public static final String[] VARIABLE_TAGS = new String[]{"ivar", "cvar", "var"}; - public static String[] RAISES_TAGS = new String[]{"raises", "raise", "except", "exception"}; - public static String[] RETURN_TAGS = new String[]{"return", "returns"}; + public static final String[] RAISES_TAGS = new String[]{"raises", "raise", "except", "exception"}; + public static final String[] RETURN_TAGS = new String[]{"return", "returns"}; @NotNull private final String myTagPrefix; - public static String TYPE = "type"; + static String TYPE = "type"; protected TagBasedDocString(@NotNull Substring docStringText, @NotNull String tagPrefix) { super(docStringText); @@ -69,8 +69,6 @@ public abstract class TagBasedDocString extends DocStringLineParser implements S myDescription = builder.toString(); } - public abstract List getAdditionalTags(); - @NotNull @Override public String getDescription() { diff --git a/python/src/com/jetbrains/python/PySearchableOptionContributor.java b/python/src/com/jetbrains/python/PySearchableOptionContributor.java index 108f4ac4906b..9b0ebb78850f 100644 --- a/python/src/com/jetbrains/python/PySearchableOptionContributor.java +++ b/python/src/com/jetbrains/python/PySearchableOptionContributor.java @@ -49,8 +49,6 @@ final class PySearchableOptionContributor extends SearchableOptionContributor { configurableId, displayName, false); processor.addOptions("reStructuredText", displayName, "Docstring format", configurableId, displayName, false); - processor.addOptions("Epytext", "Docstring format", "Docstring format", - configurableId, displayName, false); processor.addOptions("Plain", displayName, "Docstring format", configurableId, displayName, false); processor.addOptions("Unittests", displayName, "Default test runner", diff --git a/python/src/com/jetbrains/python/documentation/PyRuntimeDocstringFormatter.kt b/python/src/com/jetbrains/python/documentation/PyRuntimeDocstringFormatter.kt index 6824e5eefa53..80f914ff731e 100644 --- a/python/src/com/jetbrains/python/documentation/PyRuntimeDocstringFormatter.kt +++ b/python/src/com/jetbrains/python/documentation/PyRuntimeDocstringFormatter.kt @@ -1,6 +1,7 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.documentation +import com.google.gson.Gson import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.module.Module import com.intellij.openapi.util.text.HtmlChunk @@ -10,12 +11,17 @@ import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.PythonHelper import com.jetbrains.python.documentation.docstrings.DocStringFormat import com.jetbrains.python.sdk.PySdkUtil +import com.jetbrains.python.sdk.PySdkUtil.getLanguageLevelForSdk import com.jetbrains.python.sdk.PythonSdkType +import org.jetbrains.annotations.Nls import java.io.File object PyRuntimeDocstringFormatter { fun runExternalTool(module: Module, format: DocStringFormat, input: String, formatterFlags: List): String? { - val sdk = PythonSdkType.findLocalCPython(module) ?: return logErrorAndReturnMessage(format) + val sdk = PythonSdkType.findLocalCPython(module) ?: return logSdkNotFound(format) + if (getLanguageLevelForSdk(sdk).isPython2) { + return logPy2NotSupported() + } val sdkHome = sdk.homePath ?: return null val encodedInput = DEFAULT_CHARSET.encode(input) @@ -39,11 +45,21 @@ object PyRuntimeDocstringFormatter { else logScriptError(input) } - private fun logErrorAndReturnMessage(format: DocStringFormat): String { + private fun logErrorToJsonBody(@Nls message: String): String { + return Gson().toJson( + PyDocumentationBuilder.DocstringFormatterRequest( + HtmlChunk.p().attr("color", ColorUtil.toHtmlColor(JBColor.RED)).addRaw(message).toString())) + } + + private fun logPy2NotSupported(): String { + val message = PyPsiBundle.message("QDOC.docstring.rendering.is.not.supported.for.python.2") + LOG.warn(message) + return logErrorToJsonBody(message) + } + + private fun logSdkNotFound(format: DocStringFormat): String { LOG.warn("Python SDK for input formatter $format is not found") - val missingInterpreterMessage = PyPsiBundle.message("QDOC.local.sdk.not.found") - return HtmlChunk.p().attr("color", ColorUtil.toHtmlColor(JBColor.RED)) - .addRaw(missingInterpreterMessage).toString() + return logErrorToJsonBody(PyPsiBundle.message("QDOC.local.sdk.not.found")) } private fun logScriptError(input: String): String? { diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkType.java b/python/src/com/jetbrains/python/sdk/PythonSdkType.java index 038c8ada036a..738f6354cd95 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkType.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkType.java @@ -224,6 +224,7 @@ public final class PythonSdkType extends SdkType { return name; } } + @RequiresBackgroundThread(generateAssertion = false) //because of process output public static @Nullable String suggestBaseSdkName(@NotNull String sdkHome) { final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdkHome); @@ -539,23 +540,6 @@ public final class PythonSdkType extends SdkType { return PySdkUtil.getLanguageLevelForSdk(sdk); } - public static @Nullable Sdk findPython2Sdk(@Nullable Module module) { - final Sdk moduleSDK = PythonSdkUtil.findPythonSdk(module); - if (moduleSDK != null && getLanguageLevelForSdk(moduleSDK).isPython2()) { - return moduleSDK; - } - return findPython2Sdk(PythonSdkUtil.getAllSdks()); - } - - public static @Nullable Sdk findPython2Sdk(@NotNull List sdks) { - for (Sdk sdk : ContainerUtil.sorted(sdks, PreferredSdkComparator.INSTANCE)) { - if (getLanguageLevelForSdk(sdk).isPython2()) { - return sdk; - } - } - return null; - } - @Override public boolean allowWslSdkForLocalProject() { return true;