diff --git a/python/psi-api/src/com/jetbrains/python/toolbox/Substring.java b/python/psi-api/src/com/jetbrains/python/toolbox/Substring.java index 4b6c09fba01c..a71681f911fe 100644 --- a/python/psi-api/src/com/jetbrains/python/toolbox/Substring.java +++ b/python/psi-api/src/com/jetbrains/python/toolbox/Substring.java @@ -16,6 +16,7 @@ package com.jetbrains.python.toolbox; import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -221,11 +222,31 @@ public class Substring implements CharSequence { return new Substring(myString, start, end); } + /** + * If both substrings share the same origin, returns new substring that includes both of them. + */ + @NotNull + public Substring union(@NotNull Substring other) { + if (!myString.equals(other.myString)) { + throw new IllegalArgumentException(String.format("Substrings '%s' and '%s' must belong to the same origin", this, other)); + } + final TextRange unionRange = getTextRange().union(other.getTextRange()); + return new Substring(getSuperString(), unionRange.getStartOffset(), unionRange.getEndOffset()); + } + public int getStartOffset() { return myStartOffset; } + public int getStartLine() { + return StringUtil.offsetToLineNumber(myString, myStartOffset); + } + public int getEndOffset() { return myEndOffset; } + + public int getEndLine() { + return StringUtil.offsetToLineNumber(myString, myEndOffset); + } } diff --git a/python/src/com/jetbrains/python/documentation/DocStringLineParser.java b/python/src/com/jetbrains/python/documentation/DocStringLineParser.java index cfdfcd728f57..3278ec3488bc 100644 --- a/python/src/com/jetbrains/python/documentation/DocStringLineParser.java +++ b/python/src/com/jetbrains/python/documentation/DocStringLineParser.java @@ -58,10 +58,6 @@ public abstract class DocStringLineParser { return myLines.get(lineNum); } - public int getLineByOffset(int offset) { - return StringUtil.countNewLines(myDocStringContent.getSuperString().subSequence(0, offset)); - } - @Nullable public Substring getLineOrNull(int lineNum) { return lineNum >= 0 && lineNum < myLines.size() ? myLines.get(lineNum) : null; @@ -80,4 +76,29 @@ public abstract class DocStringLineParser { public Substring getDocStringContent() { return myDocStringContent; } + + /** + * Finds line number of the line next after the end of indented block, i.e. if returned value equals startLine, + * it means that no block with requested indentation exist. Block can contain intermediate empty lines or lines that consists + * solely of spaces: their indentation is ignored, but it always ends with non-empty line. Non-empty lines in a block must + * have indentation greater than indentThreshold and for them {@link #isBlockEnd(int)} must return false. + */ + public int parseIndentedBlock(int startLine, int indentThreshold) { + int blockEnd = startLine - 1; + int lineNum = startLine; + while (!(lineNum >= getLineCount() || isBlockEnd(lineNum))) { + if (!isEmpty(lineNum)) { + if (getLineIndentSize(lineNum) > indentThreshold) { + blockEnd = lineNum; + } + else { + break; + } + } + lineNum++; + } + return blockEnd + 1; + } + + protected abstract boolean isBlockEnd(int lineNum); } diff --git a/python/src/com/jetbrains/python/documentation/DocStringUtil.java b/python/src/com/jetbrains/python/documentation/DocStringUtil.java index 320392860333..692f124038fa 100644 --- a/python/src/com/jetbrains/python/documentation/DocStringUtil.java +++ b/python/src/com/jetbrains/python/documentation/DocStringUtil.java @@ -29,6 +29,10 @@ import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ArrayUtil; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; +import com.jetbrains.python.documentation.docstrings.DocStringUpdater; +import com.jetbrains.python.documentation.docstrings.GoogleCodeStyleDocStringUpdater; +import com.jetbrains.python.documentation.docstrings.NumpyDocStringUpdater; +import com.jetbrains.python.documentation.docstrings.TagBasedDocStringUpdater; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; @@ -262,4 +266,34 @@ public class DocStringUtil { final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(getModuleForElement(anchor)); return settings.getFormatForFile(anchor.getContainingFile()); } + + public static void removeParamFromDocString(@NotNull PyStringLiteralExpression docString, @NotNull String paramName) { + final Module module = getModuleForElement(docString); + final DocStringFormat format = PyDocumentationSettings.getInstance(module).getFormatForFile(docString.getContainingFile()); + DocStringUpdater updater; + switch (format) { + case EPYTEXT: + final EpydocString epyParsed = (EpydocString)parseDocString(format, docString); + updater = new TagBasedDocStringUpdater(epyParsed, "@", PyIndentUtil.getElementIndent(docString)); + break; + case REST: + final SphinxDocString restParsed = (SphinxDocString)parseDocString(format, docString); + updater = new TagBasedDocStringUpdater(restParsed, ":", PyIndentUtil.getElementIndent(docString)); + break; + case GOOGLE: + final GoogleCodeStyleDocString googleParsed = (GoogleCodeStyleDocString)parseDocString(format, docString); + updater = new GoogleCodeStyleDocStringUpdater(googleParsed, PyIndentUtil.getElementIndent(docString)); + break; + case NUMPY: + final NumpyDocString numpyParsed = (NumpyDocString)parseDocString(format, docString); + updater = new NumpyDocStringUpdater(numpyParsed, PyIndentUtil.getElementIndent(docString)); + break; + default: + return; + } + updater.removeParameter(paramName); + final String newText = updater.getDocStringText(); + final PyExpressionStatement replacement = PyElementGenerator.getInstance(docString.getProject()).createDocstring(newText); + docString.replace(replacement.getExpression()); + } } diff --git a/python/src/com/jetbrains/python/documentation/GoogleCodeStyleDocString.java b/python/src/com/jetbrains/python/documentation/GoogleCodeStyleDocString.java index e168c8c01801..67c98ef58943 100644 --- a/python/src/com/jetbrains/python/documentation/GoogleCodeStyleDocString.java +++ b/python/src/com/jetbrains/python/documentation/GoogleCodeStyleDocString.java @@ -104,7 +104,7 @@ public class GoogleCodeStyleDocString extends SectionBasedDocString { final List nestedBlock = pair.getFirst(); if (!nestedBlock.isEmpty()) { //noinspection ConstantConditions - description = mergeSubstrings(description, ContainerUtil.getLastItem(nestedBlock)); + description = description.union(ContainerUtil.getLastItem(nestedBlock)); } description = description.trim(); return Pair.create(new SectionField(name, type, description), pair.getSecond()); diff --git a/python/src/com/jetbrains/python/documentation/NumpyDocString.java b/python/src/com/jetbrains/python/documentation/NumpyDocString.java index 590ff46f0548..2ce118237ea2 100644 --- a/python/src/com/jetbrains/python/documentation/NumpyDocString.java +++ b/python/src/com/jetbrains/python/documentation/NumpyDocString.java @@ -85,7 +85,7 @@ public class NumpyDocString extends SectionBasedDocString { final Pair, Integer> parsedDescription = parseIndentedBlock(lineNum + 1, getLineIndentSize(lineNum), sectionIndent); final List descriptionLines = parsedDescription.getFirst(); if (!descriptionLines.isEmpty()) { - description = mergeSubstrings(descriptionLines.get(0), descriptionLines.get(descriptionLines.size() - 1)); + description = descriptionLines.get(0).union(descriptionLines.get(descriptionLines.size() - 1)); } return Pair.create(new SectionField(name, type, description != null ? description.trim() : null), parsedDescription.getSecond()); } @@ -96,7 +96,7 @@ public class NumpyDocString extends SectionBasedDocString { } @Override - protected boolean isSectionBreak(int lineNum, int curSectionIndent) { - return super.isSectionBreak(lineNum, curSectionIndent) || (isEmpty(lineNum) && isEmptyOrDoesNotExist(lineNum + 1)); + protected boolean isBlockEnd(int lineNum) { + return super.isBlockEnd(lineNum) || (isEmpty(lineNum) && isEmptyOrDoesNotExist(lineNum + 1)); } } diff --git a/python/src/com/jetbrains/python/documentation/PyDocstringGenerator.java b/python/src/com/jetbrains/python/documentation/PyDocstringGenerator.java index faf90326ca7b..c363adcfe9cd 100644 --- a/python/src/com/jetbrains/python/documentation/PyDocstringGenerator.java +++ b/python/src/com/jetbrains/python/documentation/PyDocstringGenerator.java @@ -371,7 +371,7 @@ public class PyDocstringGenerator { if (updater != null) { for (DocstringParam param : myParams) { if (param.isReturnValue()) { - updater.addReturnValue((param.getType())); + updater.addReturnValue(param.getType()); } else { updater.addParameter(param.getName(), param.getType()); diff --git a/python/src/com/jetbrains/python/documentation/SectionBasedDocString.java b/python/src/com/jetbrains/python/documentation/SectionBasedDocString.java index 87bceb127f13..0cf2f0c92362 100644 --- a/python/src/com/jetbrains/python/documentation/SectionBasedDocString.java +++ b/python/src/com/jetbrains/python/documentation/SectionBasedDocString.java @@ -120,7 +120,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen lineNum = skipEmptyLines(lineNum); } //noinspection ConstantConditions - mySummary = summary.isEmpty() ? null : mergeSubstrings(summary.get(0), summary.get(summary.size() - 1)).trim(); + mySummary = summary.isEmpty() ? null : summary.get(0).union(summary.get(summary.size() - 1)).trim(); } @NotNull @@ -197,7 +197,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen final Substring firstLine = ContainerUtil.getFirstItem(pair.getFirst()); final Substring lastLine = ContainerUtil.getLastItem(pair.getFirst()); if (firstLine != null && lastLine != null) { - return Pair.create(new SectionField(null, null, mergeSubstrings(firstLine, lastLine).trim()), pair.getSecond()); + return Pair.create(new SectionField(null, null, firstLine.union(lastLine).trim()), pair.getSecond()); } return Pair.create(null, pair.getSecond()); } @@ -218,9 +218,9 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen } protected boolean isSectionBreak(int lineNum, int curSectionIndent) { - return lineNum >= getLineCount() || - isSectionStart(lineNum) || - (!isEmpty(lineNum) && getLineIndentSize(lineNum) <= curSectionIndent); + return lineNum >= getLineCount() || + (!isEmpty(lineNum) && getLineIndentSize(lineNum) <= curSectionIndent) || + isBlockEnd(lineNum); } /** @@ -231,22 +231,13 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen */ @NotNull protected Pair, Integer> parseIndentedBlock(int lineNum, int blockIndent, int sectionIndent) { - final List result = new ArrayList(); - int lastNonEmpty = lineNum - 1; - while (!isSectionBreak(lineNum, sectionIndent)) { - if (getLineIndentSize(lineNum) > blockIndent) { - // copy all lines after the last non empty including the current one - for (int i = lastNonEmpty + 1; i <= lineNum; i++) { - result.add(getLine(i)); - } - lastNonEmpty = lineNum; - } - else if (!isEmpty(lineNum)) { - break; - } - lineNum++; - } - return Pair.create(result, lineNum); + final int blockEnd = parseIndentedBlock(lineNum, blockIndent); + return Pair.create(myLines.subList(lineNum, blockEnd), blockEnd); + } + + @Override + protected boolean isBlockEnd(int lineNum) { + return isSectionStart(lineNum); } /** @@ -274,22 +265,6 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen return line.split(":", 1); } - /** - * If both substrings share the same origin, returns new substring that includes both of them. Otherwise return {@code null}. - * - * @param s2 substring to concat with - * @return new substring as described - */ - @NotNull - protected static Substring mergeSubstrings(@NotNull Substring s1, @NotNull Substring s2) { - if (!s1.getSuperString().equals(s2.getSuperString())) { - throw new IllegalArgumentException(String.format("Substrings '%s' and '%s' must belong to the same origin", s1, s2)); - } - return new Substring(s1.getSuperString(), - Math.min(s1.getStartOffset(), s2.getStartOffset()), - Math.max(s1.getEndOffset(), s2.getEndOffset())); - } - // like Python's textwrap.dedent() @NotNull protected static String stripCommonIndent(@NotNull Substring text, boolean ignoreFirstStringIfNonEmpty) { diff --git a/python/src/com/jetbrains/python/documentation/TagBasedDocString.java b/python/src/com/jetbrains/python/documentation/TagBasedDocString.java index 432d1fc0c18b..663bb6bb5497 100644 --- a/python/src/com/jetbrains/python/documentation/TagBasedDocString.java +++ b/python/src/com/jetbrains/python/documentation/TagBasedDocString.java @@ -217,4 +217,9 @@ public abstract class TagBasedDocString extends DocStringLineParser implements S results.addAll(getTagArguments(PARAM_TYPE_TAGS)); return results; } + + @Override + protected boolean isBlockEnd(int lineNum) { + return getLine(lineNum).trimLeft().startsWith(myTagPrefix); + } } diff --git a/python/src/com/jetbrains/python/documentation/docstrings/DocStringUpdater.java b/python/src/com/jetbrains/python/documentation/docstrings/DocStringUpdater.java index 32caf5d50767..9b4dd65da8c4 100644 --- a/python/src/com/jetbrains/python/documentation/docstrings/DocStringUpdater.java +++ b/python/src/com/jetbrains/python/documentation/docstrings/DocStringUpdater.java @@ -59,6 +59,21 @@ public abstract class DocStringUpdater { insert(line.getEndOffset(), '\n' + text); } + protected final void remove(int startOffset, int endOffset) { + replace(startOffset, endOffset, ""); + } + + protected final void removeLines(int startLine, int endLine) { + final List lines = myOriginalDocString.getLines(); + final int startOffset = lines.get(startLine).getStartOffset(); + final int endOffset = endLine < lines.size() - 1 ? lines.get(endLine + 1).getStartOffset() : lines.get(endLine).getEndOffset(); + remove(startOffset, endOffset); + } + + protected final void removeLine(int line) { + removeLines(line, line); + } + protected final void insertBeforeLine(int lineNumber, @NotNull String text) { final Substring line = myOriginalDocString.getLines().get(lineNumber); insert(line.getStartOffset(), text + '\n'); @@ -100,6 +115,10 @@ public abstract class DocStringUpdater { return lastLineIndent; } + protected int getLineIndentSize(int lineNum) { + return PyIndentUtil.getLineIndentSize(getLineIndent(lineNum)); + } + protected int findLastNonEmptyLine() { for (int i = myOriginalDocString.getLineCount() - 1; i >= 0; i--) { if (!StringUtil.isEmptyOrSpaces(myOriginalDocString.getLine(i))) { @@ -113,6 +132,8 @@ public abstract class DocStringUpdater { public abstract void addReturnValue(@Nullable String type); + public abstract void removeParameter(@NotNull String name); + private static class Modification implements Comparable { @NotNull final TextRange range; @NotNull final String text; diff --git a/python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringUpdater.java b/python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringUpdater.java index e9707bdd44d3..bb530817eba0 100644 --- a/python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringUpdater.java +++ b/python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringUpdater.java @@ -116,6 +116,15 @@ public abstract class SectionBasedDocStringUpdater extends DocStringUpdater extends DocStringUpdater{ +public class TagBasedDocStringUpdater extends DocStringUpdater{ private final String myTagPrefix; - public TagBasedDocStringUpdater(@NotNull T docString, @NotNull String tagPrefix, @NotNull String minContentIndent) { + public TagBasedDocStringUpdater(@NotNull TagBasedDocString docString, @NotNull String tagPrefix, @NotNull String minContentIndent) { super(docString, minContentIndent); myTagPrefix = tagPrefix; } @@ -57,6 +59,23 @@ public class TagBasedDocStringUpdater extends DocSt } } + @Override + public void removeParameter(@NotNull String name) { + final List nameSubs = myOriginalDocString.getParameterSubstrings(); + for (Substring sub : nameSubs) { + if (sub.toString().equals(name)) { + final int startLine = sub.getStartLine(); + final int nextAfterBlock = myOriginalDocString.parseIndentedBlock(startLine + 1, getLineIndentSize(startLine)); + if (nextAfterBlock != startLine + 1) { + removeLines(startLine, nextAfterBlock - 1); + } + else { + removeLine(startLine); + } + } + } + } + private void insertTagLine(@NotNull DocStringBuilder lineBuilder) { final int firstLineWithTag = findFirstLineWithTag(); if (firstLineWithTag >= 0) { diff --git a/python/src/com/jetbrains/python/inspections/quickfix/DocstringQuickFix.java b/python/src/com/jetbrains/python/inspections/quickfix/DocstringQuickFix.java index 77c425472d70..40ffa24033af 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/DocstringQuickFix.java +++ b/python/src/com/jetbrains/python/inspections/quickfix/DocstringQuickFix.java @@ -20,19 +20,18 @@ import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.python.PyBundle; import com.jetbrains.python.codeInsight.intentions.PyGenerateDocstringIntention; +import com.jetbrains.python.documentation.DocStringUtil; import com.jetbrains.python.documentation.PyDocstringGenerator; -import com.jetbrains.python.documentation.PyDocumentationSettings; -import com.jetbrains.python.editor.PythonDocCommentUtil; -import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyDocStringOwner; +import com.jetbrains.python.psi.PyFunction; +import com.jetbrains.python.psi.PyStringLiteralExpression; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -90,27 +89,11 @@ public class DocstringQuickFix implements LocalQuickFix { return; } if (docStringExpression != null) { - PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); - final Module module = ModuleUtilCore.findModuleForPsiElement(docStringExpression); - if (module == null) return; - PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module); - if (documentationSettings.isEpydocFormat(docStringExpression.getContainingFile())) { - myPrefix = "@"; - } - else { - myPrefix = ":"; - } - - String replacement = docStringExpression.getText(); if (myMissingText != null) { - replacement = new PyDocstringGenerator(docStringOwner).withParam(myMissingText).buildDocString(); + new PyDocstringGenerator(docStringOwner).withParam(myMissingText).buildAndInsert(); } - if (myUnexpected != null) { - replacement = PythonDocCommentUtil.removeParamFromDocstring(replacement, myPrefix, myUnexpected); - } - if (!replacement.equals(docStringExpression.getText()) && !StringUtil.isEmptyOrSpaces(replacement)) { - PyExpression str = elementGenerator.createDocstring(replacement).getExpression(); - docStringExpression.replace(str); + else if (myUnexpected != null) { + DocStringUtil.removeParamFromDocString(docStringExpression, myUnexpected); } } } diff --git a/python/src/com/jetbrains/python/inspections/quickfix/PyRemoveParameterQuickFix.java b/python/src/com/jetbrains/python/inspections/quickfix/PyRemoveParameterQuickFix.java index ed2f985af153..663d6a457e83 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/PyRemoveParameterQuickFix.java +++ b/python/src/com/jetbrains/python/inspections/quickfix/PyRemoveParameterQuickFix.java @@ -17,15 +17,12 @@ package com.jetbrains.python.inspections.quickfix; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ProblemDescriptor; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.usageView.UsageInfo; import com.jetbrains.python.PyBundle; -import com.jetbrains.python.documentation.PyDocumentationSettings; -import com.jetbrains.python.editor.PythonDocCommentUtil; +import com.jetbrains.python.documentation.DocStringUtil; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.refactoring.PyRefactoringUtil; @@ -76,14 +73,7 @@ public class PyRemoveParameterQuickFix implements LocalQuickFix { final PyStringLiteralExpression expression = pyFunction.getDocStringExpression(); final String paramName = ((PyParameter)element).getName(); if (expression != null && paramName != null) { - final Module module = ModuleUtilCore.findModuleForPsiElement(pyFunction); - assert module != null; - PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module); - String prefix = documentationSettings.isEpydocFormat(pyFunction.getContainingFile()) ? "@" : ":"; - final String replacement = PythonDocCommentUtil.removeParamFromDocstring(expression.getText(), prefix, paramName); - PyExpression str = - PyElementGenerator.getInstance(project).createDocstring(replacement).getExpression(); - expression.replace(str); + DocStringUtil.removeParamFromDocString(expression, paramName); } } diff --git a/python/src/com/jetbrains/python/refactoring/changeSignature/PyChangeSignatureUsageProcessor.java b/python/src/com/jetbrains/python/refactoring/changeSignature/PyChangeSignatureUsageProcessor.java index 66da653c29b4..d108f416801f 100644 --- a/python/src/com/jetbrains/python/refactoring/changeSignature/PyChangeSignatureUsageProcessor.java +++ b/python/src/com/jetbrains/python/refactoring/changeSignature/PyChangeSignatureUsageProcessor.java @@ -33,9 +33,8 @@ import com.intellij.util.containers.HashSet; import com.intellij.util.containers.MultiMap; import com.jetbrains.python.PyNames; import com.jetbrains.python.PythonLanguage; +import com.jetbrains.python.documentation.DocStringUtil; import com.jetbrains.python.documentation.PyDocstringGenerator; -import com.jetbrains.python.documentation.PyDocumentationSettings; -import com.jetbrains.python.editor.PythonDocCommentUtil; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.search.PyOverridingMethodsSearch; import com.jetbrains.python.refactoring.PyRefactoringUtil; @@ -340,13 +339,7 @@ public class PyChangeSignatureUsageProcessor implements ChangeSignatureUsageProc for (PyParameter p : function.getParameterList().getParameters()) { final String paramName = p.getName(); if (!names.contains(paramName) && paramName != null) { - PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module); - String prefix = documentationSettings.isEpydocFormat(docStringExpression.getContainingFile()) ? "@" : ":"; - final String replacement = PythonDocCommentUtil.removeParamFromDocstring(docStringExpression.getText(), prefix, - paramName); - PyExpression str = - PyElementGenerator.getInstance(function.getProject()).createDocstring(replacement).getExpression(); - docStringExpression.replace(str); + DocStringUtil.removeParamFromDocString(docStringExpression, paramName); } } }