diff --git a/python/psi-api/src/com/jetbrains/python/toolbox/PyIndentUtil.java b/python/psi-api/src/com/jetbrains/python/toolbox/PyIndentUtil.java new file mode 100644 index 000000000000..c8231b30f1b7 --- /dev/null +++ b/python/psi-api/src/com/jetbrains/python/toolbox/PyIndentUtil.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2015 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.toolbox; + +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class PyIndentUtil { + private PyIndentUtil() { + } + + public static int getLineIndentSize(@NotNull CharSequence line) { + return getLineIndent(line).length(); + } + + @NotNull + public static CharSequence getLineIndent(@NotNull CharSequence line) { + int stop; + for (stop = 0; stop < line.length(); stop++) { + final char c = line.charAt(stop); + if (c == ' ' || c == '\t') { + break; + } + } + return line.subSequence(0, stop); + } +} diff --git a/python/src/com/jetbrains/python/documentation/DocStringFormat.java b/python/src/com/jetbrains/python/documentation/DocStringFormat.java index f5527f61cdea..41d902c19067 100644 --- a/python/src/com/jetbrains/python/documentation/DocStringFormat.java +++ b/python/src/com/jetbrains/python/documentation/DocStringFormat.java @@ -15,9 +15,16 @@ */ package com.jetbrains.python.documentation; +import com.intellij.psi.PsiElement; import com.intellij.util.Function; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.python.documentation.docstrings.DocStringBuilder; +import com.jetbrains.python.documentation.docstrings.DocStringProvider; +import com.jetbrains.python.documentation.docstrings.DocStringUpdater; +import com.jetbrains.python.documentation.docstrings.SphinxDocstringProvider; +import com.jetbrains.python.psi.StructuredDocString; +import com.jetbrains.python.toolbox.Substring; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -28,14 +35,22 @@ import java.util.List; * @author yole */ public enum DocStringFormat { + /** + * @see DocStringUtil#ensureNotPlainDocstringFormat(PsiElement) + */ PLAIN("Plain"), EPYTEXT("Epytext"), - REST("reStructuredText"), + REST("reStructuredText") { + @NotNull + @Override + public DocStringProvider getProvider() { + return new SphinxDocstringProvider(); + } + }, NUMPY("NumPy"), GOOGLE("Google"); public static final List ALL_NAMES = getAllNames(); - public static final List ALL_NAMES_BUT_PLAIN = getAllNamesButPlain(); @NotNull private static List getAllNames() { @@ -47,6 +62,8 @@ public enum DocStringFormat { })); } + public static final List ALL_NAMES_BUT_PLAIN = getAllNamesButPlain(); + @NotNull private static List getAllNamesButPlain() { return Collections.unmodifiableList(ContainerUtil.mapNotNull(values(), new Function() { @@ -73,6 +90,7 @@ public enum DocStringFormat { } String myName; + DocStringFormat(@NotNull String name) { myName = name; } @@ -81,4 +99,28 @@ public enum DocStringFormat { public String getName() { return myName; } + + @NotNull + public DocStringProvider getProvider() { + return new StubDocStringProvider(); + } + + private static class StubDocStringProvider extends DocStringProvider { + @Override + public StructuredDocString parseDocString(@NotNull Substring content) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public DocStringUpdater updateDocString(@NotNull StructuredDocString docstring) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public DocStringBuilder createDocString() { + throw new UnsupportedOperationException(); + } + } } diff --git a/python/src/com/jetbrains/python/documentation/DocStringLineParser.java b/python/src/com/jetbrains/python/documentation/DocStringLineParser.java new file mode 100644 index 000000000000..61c81129e2c7 --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/DocStringLineParser.java @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2015 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; + +import com.intellij.openapi.util.text.StringUtil; +import com.jetbrains.python.toolbox.PyIndentUtil; +import com.jetbrains.python.toolbox.Substring; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public abstract class DocStringLineParser { + protected final List myLines; + protected final Substring myDocStringContent; + + protected DocStringLineParser(@NotNull Substring content) { + myDocStringContent = content; + myLines = Collections.unmodifiableList(content.splitLines()); + } + + protected static int getIndent(@NotNull CharSequence line) { + return PyIndentUtil.getLineIndentSize(line); + } + + public boolean isEmptyOrDoesNotExist(int lineNum) { + return lineNum < 0 || lineNum >= myLines.size() || isEmpty(lineNum); + } + + public boolean isEmpty(int lineNum) { + return StringUtil.isEmptyOrSpaces(getLine(lineNum)); + } + + @NotNull + public Substring getLine(int lineNum) { + return myLines.get(lineNum); + } + + public int getLineIndent(int lineNum) { + return getIndent(myLines.get(lineNum)); + } + + public int getLineByOffset(int offset) { + return StringUtil.countNewLines(myDocStringContent.subSequence(0, offset)); + } + + @Nullable + public Substring getLineOrNull(int lineNum) { + return lineNum >= 0 && lineNum < myLines.size() ? myLines.get(lineNum) : null; + } + + public int getLineCount() { + return myLines.size(); + } + + @NotNull + public List getLines() { + return myLines; + } + + @NotNull + public Substring getDocStringContent() { + return myDocStringContent; + } +} diff --git a/python/src/com/jetbrains/python/documentation/DocStringUtil.java b/python/src/com/jetbrains/python/documentation/DocStringUtil.java index 37a3c6c64f27..156608bc95ca 100644 --- a/python/src/com/jetbrains/python/documentation/DocStringUtil.java +++ b/python/src/com/jetbrains/python/documentation/DocStringUtil.java @@ -54,7 +54,7 @@ public class DocStringUtil { return null; } if (isSphinxDocString(text)) { - return new SphinxDocString(text); + return DocStringFormat.REST.getProvider().parseDocStringContent(text); } if (isGoogleDocString(text)) { return new GoogleCodeStyleDocString(text); @@ -62,7 +62,7 @@ public class DocStringUtil { if (isNumpyDocstring(text)) { return new NumpyDocString(text); } - return new EpydocString(text); + return DocStringFormat.EPYTEXT.getProvider().parseDocStringContent(text); } public static boolean isSphinxDocString(@NotNull String text) { diff --git a/python/src/com/jetbrains/python/documentation/EpydocString.java b/python/src/com/jetbrains/python/documentation/EpydocString.java index 24bedfc70ae4..9e351ab6c656 100644 --- a/python/src/com/jetbrains/python/documentation/EpydocString.java +++ b/python/src/com/jetbrains/python/documentation/EpydocString.java @@ -46,14 +46,7 @@ public class EpydocString extends TagBasedDocString { "precondition", "postcondition", "invariant", "author", "organization", "copyright", "license", "contact", "summary", "see" }; - /** - * Empty doc (for {@link #createParameterType(String, String)} probably) - */ - public EpydocString() { - this(""); - } - - public EpydocString(@NotNull String docstringText) { + public EpydocString(@NotNull Substring docstringText) { super(docstringText, "@"); } diff --git a/python/src/com/jetbrains/python/documentation/PyDocumentationSettings.java b/python/src/com/jetbrains/python/documentation/PyDocumentationSettings.java index 0faad3fd7f24..dc15854fc7e5 100644 --- a/python/src/com/jetbrains/python/documentation/PyDocumentationSettings.java +++ b/python/src/com/jetbrains/python/documentation/PyDocumentationSettings.java @@ -143,10 +143,10 @@ public class PyDocumentationSettings implements PersistentStateComponentNapoleon */ -public abstract class SectionBasedDocString implements StructuredDocString { +public abstract class SectionBasedDocString extends DocStringLineParser implements StructuredDocString { /** * Frequently used section types @@ -88,18 +88,16 @@ public abstract class SectionBasedDocString implements StructuredDocString { private static final ImmutableSet SECTIONS_WITH_TYPE = ImmutableSet.of(RAISES_SECTION); private static final ImmutableSet SECTIONS_WITH_NAME = ImmutableSet.of(METHODS_SECTION); - protected final List myLines; - private final Substring mySummary; private final List
mySections = new ArrayList
(); private final List myOtherContent = new ArrayList(); protected SectionBasedDocString(@NotNull String text) { - myLines = new Substring(text).splitLines(); + super(new Substring(text)); List summary = Collections.emptyList(); int startLine = skipEmptyLines(parseHeader(0)); int lineNum = startLine; - while (lineNum < myLines.size()) { + while (lineNum < getLineCount()) { final Pair parsedSection = parseSection(lineNum); if (parsedSection.getFirst() != null) { mySections.add(parsedSection.getFirst()); @@ -200,7 +198,7 @@ public abstract class SectionBasedDocString implements StructuredDocString { protected abstract Pair parseSectionHeader(int lineNum); protected int skipEmptyLines(int lineNum) { - while (lineNum < myLines.size() && isEmpty(lineNum)) { + while (lineNum < getLineCount() && isEmpty(lineNum)) { lineNum++; } return lineNum; @@ -211,21 +209,13 @@ public abstract class SectionBasedDocString implements StructuredDocString { return title == null ? null : SECTION_ALIASES.get(title.toLowerCase()); } - protected boolean isEmptyOrDoesNotExist(int lineNum) { - return lineNum < 0 || lineNum >= myLines.size() || isEmpty(lineNum); - } - - protected boolean isEmpty(int lineNum) { - return StringUtil.isEmptyOrSpaces(getLine(lineNum)); - } - protected boolean isSectionStart(int lineNum) { final Pair pair = parseSectionHeader(lineNum); return pair.getFirst() != null; } protected boolean isSectionBreak(int lineNum, int curSectionIndent) { - return lineNum >= myLines.size() || + return lineNum >= getLineCount() || isSectionStart(lineNum) || (!isEmpty(lineNum) && getIndent(getLine(lineNum)) <= curSectionIndent); } @@ -288,7 +278,7 @@ public abstract class SectionBasedDocString implements StructuredDocString { * @return new substring as described */ @NotNull - public static Substring mergeSubstrings(@NotNull Substring s1, @NotNull Substring s2) { + 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)); } @@ -324,25 +314,6 @@ public abstract class SectionBasedDocString implements StructuredDocString { return StringUtil.join(skipFirstLine ? ContainerUtil.prepend(dedentedLines, firstLine) : dedentedLines, "\n"); } - protected static int getIndent(@NotNull CharSequence line) { - for (int i = 0; i < line.length(); i++) { - if (!Character.isSpaceChar(line.charAt(i))) { - return i; - } - } - return 0; - } - - @NotNull - protected Substring getLine(int lineNum) { - return myLines.get(lineNum); - } - - @Nullable - protected Substring getLineOrNull(int lineNum) { - return lineNum >= 0 && lineNum < myLines.size() ? myLines.get(lineNum) : null; - } - @VisibleForTesting public List
getSections() { return Collections.unmodifiableList(mySections); diff --git a/python/src/com/jetbrains/python/documentation/SphinxDocString.java b/python/src/com/jetbrains/python/documentation/SphinxDocString.java index 6c7d74108431..1ab7068e35ac 100644 --- a/python/src/com/jetbrains/python/documentation/SphinxDocString.java +++ b/python/src/com/jetbrains/python/documentation/SphinxDocString.java @@ -31,14 +31,7 @@ public class SphinxDocString extends TagBasedDocString { ":type", ":raise", ":raises", ":var", ":cvar", ":ivar", ":return", ":returns", ":rtype", ":except", ":exception" }; - /** - * Empty doc (for {@link #createParameterType(String, String)} probably) - */ - public SphinxDocString() { - this(""); - } - - public SphinxDocString(@NotNull final String docstringText) { + public SphinxDocString(@NotNull final Substring docstringText) { super(docstringText, ":"); } diff --git a/python/src/com/jetbrains/python/documentation/TagBasedDocString.java b/python/src/com/jetbrains/python/documentation/TagBasedDocString.java index bcff4af7afa5..16581c44516b 100644 --- a/python/src/com/jetbrains/python/documentation/TagBasedDocString.java +++ b/python/src/com/jetbrains/python/documentation/TagBasedDocString.java @@ -33,7 +33,7 @@ import java.util.regex.Pattern; /** * @author yole */ -public abstract class TagBasedDocString implements StructuredDocString { +public abstract class TagBasedDocString extends DocStringLineParser implements StructuredDocString { protected final String myDescription; protected final Map mySimpleTagValues = Maps.newHashMap(); @@ -56,17 +56,15 @@ public abstract class TagBasedDocString implements StructuredDocString { public static String TYPE = "type"; - protected TagBasedDocString(@NotNull String docStringText, String tagPrefix) { + protected TagBasedDocString(@NotNull Substring docStringText, @NotNull String tagPrefix) { + super(docStringText); myTagPrefix = tagPrefix; - final Substring docString = new Substring(docStringText); - final List lines = docString.splitLines(); - final int nlines = lines.size(); final StringBuilder builder = new StringBuilder(); int lineno = 0; - while (lineno < nlines) { - Substring line = lines.get(lineno).trim(); + while (lineno < getLineCount()) { + Substring line = getLine(lineno).trim(); if (line.startsWith(tagPrefix)) { - lineno = parseTag(lines, lineno, tagPrefix); + lineno = parseTag(lineno, tagPrefix); } else { builder.append(line.toString()).append("\n"); @@ -110,8 +108,8 @@ public abstract class TagBasedDocString implements StructuredDocString { return map; } - protected int parseTag(List lines, int lineno, String tagPrefix) { - final Substring lineWithPrefix = lines.get(lineno).trimLeft(); + protected int parseTag(int lineno, String tagPrefix) { + final Substring lineWithPrefix = getLine(lineno).trimLeft(); if (lineWithPrefix.startsWith(tagPrefix)) { final Substring line = lineWithPrefix.substring(tagPrefix.length()); final Matcher strictTagMatcher = RE_STRICT_TAG_LINE.matcher(line); @@ -127,11 +125,11 @@ public abstract class TagBasedDocString implements StructuredDocString { final Substring tagName = line.getMatcherGroup(tagMatcher, 1); final Substring argName = line.getMatcherGroup(tagMatcher, 2).trim(); final TextRange firstArgLineRange = line.getMatcherGroup(tagMatcher, 3).trim().getTextRange(); - final int linesCount = lines.size(); + final int linesCount = getLineCount(); final int argStart = firstArgLineRange.getStartOffset(); int argEnd = firstArgLineRange.getEndOffset(); while (lineno + 1 < linesCount) { - final Substring nextLine = lines.get(lineno + 1).trim(); + final Substring nextLine = getLine(lineno + 1).trim(); if (nextLine.length() == 0 || nextLine.startsWith(tagPrefix)) { break; } diff --git a/python/src/com/jetbrains/python/documentation/docstrings/DocStringBuilder.java b/python/src/com/jetbrains/python/documentation/docstrings/DocStringBuilder.java new file mode 100644 index 000000000000..d781bcee760b --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/docstrings/DocStringBuilder.java @@ -0,0 +1,89 @@ +/* + * Copyright 2000-2015 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.openapi.util.text.StringUtil; +import org.intellij.lang.annotations.PrintFormat; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public abstract class DocStringBuilder { + private final List myLines; + public DocStringBuilder() { + myLines = new ArrayList(); + } + + @NotNull + public DocStringBuilder addSummary(@NotNull String summary) { + addLine(summary); + addLine(""); + return this; + } + + @NotNull + public DocStringBuilder startParameterSection() { + return this; + } + @NotNull + public abstract DocStringBuilder addParameter(@NotNull String name, @Nullable String type); + + public abstract DocStringBuilder addParameterType(@NotNull String name, @NotNull String type); + + @NotNull + public DocStringBuilder startReturnValueSection() { + return this; + } + @NotNull + public abstract DocStringBuilder addReturnValue(@Nullable String name, @NotNull String type); + + @NotNull + protected DocStringBuilder addLine(@NotNull String line) { + myLines.add(line); + return this; + } + + @NotNull + protected DocStringBuilder addLine(@NotNull @PrintFormat String format, @NotNull Object... args) { + myLines.add(String.format(format, args)); + return this; + } + + @NotNull + public String buildContent(int indent, boolean indentFirst) { + final StringBuilder result = new StringBuilder(); + if (!indentFirst && !myLines.isEmpty()) { + result.append(myLines.get(0)).append('\n'); + } + boolean first = true; + String indentation = StringUtil.repeat(" ", indent); + for (int i = indentFirst ? 0 : 1; i < myLines.size(); i++) { + if (first) { + first = false; + } + else { + result.append('\n'); + } + result.append(indentation).append(myLines.get(i)); + } + return result.toString(); + } +} diff --git a/python/src/com/jetbrains/python/documentation/docstrings/DocStringProvider.java b/python/src/com/jetbrains/python/documentation/docstrings/DocStringProvider.java new file mode 100644 index 000000000000..e7b6cf9ee37c --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/docstrings/DocStringProvider.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2015 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.google.common.base.Preconditions; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; +import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.psi.PyStringLiteralExpression; +import com.jetbrains.python.psi.StructuredDocString; +import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; +import com.jetbrains.python.toolbox.Substring; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public abstract class DocStringProvider { + public abstract T parseDocString(@NotNull Substring content); + + @NotNull + public T parseDocString(@NotNull PyStringLiteralExpression literalExpression) { + return parseDocString(literalExpression.getStringNodes().get(0)); + } + + @NotNull + public T parseDocString(@NotNull ASTNode node) { + Preconditions.checkArgument(node.getElementType() == PyTokenTypes.DOCSTRING); + return parseDocString(node.getText()); + } + + + public T parseDocString(@NotNull String stringText) { + return parseDocString(stripSuffixAndQuotes(stringText)); + } + + public T parseDocStringContent(@NotNull String stringContent) { + return parseDocString(new Substring(stringContent)); + } + + @NotNull + private static Substring stripSuffixAndQuotes(@NotNull String text) { + final TextRange contentRange = PyStringLiteralExpressionImpl.getNodeTextRange(text); + return new Substring(text, contentRange.getStartOffset(), contentRange.getEndOffset()); + } + + @NotNull + public abstract DocStringUpdater updateDocString(@NotNull T docstring); + + @NotNull + public abstract DocStringBuilder createDocString(); + +} diff --git a/python/src/com/jetbrains/python/documentation/docstrings/DocStringUpdater.java b/python/src/com/jetbrains/python/documentation/docstrings/DocStringUpdater.java new file mode 100644 index 000000000000..d1a266bf4a23 --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/docstrings/DocStringUpdater.java @@ -0,0 +1,123 @@ +/* + * Copyright 2000-2015 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.openapi.util.TextRange; +import com.jetbrains.python.documentation.DocStringLineParser; +import com.jetbrains.python.toolbox.Substring; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public abstract class DocStringUpdater { + protected final T myOriginalDocString; + private final StringBuilder myBuilder; + private final List myUpdates = new ArrayList(); + protected final List myAddParameterRequests = new ArrayList(); + protected final List myAddReturnTypeRequests = new ArrayList(); + + public DocStringUpdater(@NotNull T docString) { + myBuilder = new StringBuilder(docString.getDocStringContent().getSuperString()); + myOriginalDocString = docString; + } + + public final void addParameter(@NotNull String name, @Nullable String type) { + myAddParameterRequests.add(new AddParameter(name, type)); + } + + public final void addReturnType(@Nullable String name, @NotNull String type) { + myAddReturnTypeRequests.add(new AddReturnType(name, type)); + } + + protected void insert(int offset, @NotNull String text) { + myUpdates.add(new UpdateOperation(TextRange.from(offset, 0), text)); + } + + protected final void insertAfterLine(int lineNumber, @NotNull String text) { + final Substring line = myOriginalDocString.getLines().get(lineNumber); + insert(line.getEndOffset(), "\n" + text); + } + + protected final void replace(int startOffset, int endOffset, @NotNull String text) { + replace(new TextRange(startOffset, endOffset), text); + } + + protected final void replace(@NotNull TextRange range, @NotNull String text) { + myUpdates.add(new UpdateOperation(range, text)); + } + + protected abstract void scheduleUpdates(); + + @NotNull + public final String getDocStringText() { + scheduleUpdates(); + // if several updates insert in one place (e.g. new field), insert them in backward order + Collections.reverse(myUpdates); + Collections.sort(myUpdates); + for (int i = myUpdates.size() - 1; i >= 0; i--) { + final UpdateOperation update = myUpdates.get(i); + final TextRange updateRange = update.range; + if (updateRange.getStartOffset() == updateRange.getEndOffset()) { + myBuilder.insert(updateRange.getStartOffset(), update.text); + } + else { + myBuilder.replace(updateRange.getStartOffset(), updateRange.getEndOffset(), update.text); + } + } + return myBuilder.toString(); + } + + protected static class AddParameter { + @NotNull final String name; + @Nullable final String type; + + public AddParameter(@NotNull String name, @Nullable String type) { + this.name = name; + this.type = type; + } + } + + protected static class AddReturnType { + @Nullable final String name; + @NotNull final String type; + + public AddReturnType(@Nullable String name, @NotNull String type) { + this.name = name; + this.type = type; + } + } + + private static class UpdateOperation implements Comparable { + @NotNull final TextRange range; + @NotNull final String text; + + public UpdateOperation(@NotNull TextRange range, @NotNull String newText) { + this.range = range; + this.text = newText; + } + + @Override + public int compareTo(UpdateOperation o) { + return range.getStartOffset() - o.range.getStartOffset(); + } + } +} diff --git a/python/src/com/jetbrains/python/documentation/docstrings/EpydocDocStringProvider.java b/python/src/com/jetbrains/python/documentation/docstrings/EpydocDocStringProvider.java new file mode 100644 index 000000000000..a58d12b4ea8d --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/docstrings/EpydocDocStringProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2015 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.documentation.EpydocString; +import com.jetbrains.python.toolbox.Substring; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class EpydocDocStringProvider extends DocStringProvider { + + public static final String TAG_PREFIX = "@"; + + @Override + public EpydocString parseDocString(@NotNull Substring content) { + return new EpydocString(content); + } + + @NotNull + @Override + public DocStringUpdater updateDocString(@NotNull EpydocString docstring) { + return new TagBasedDocStringUpdater(docstring, TAG_PREFIX) { + @Override + public DocStringBuilder createDocStringBuilder() { + return createDocString(); + } + }; + } + + @NotNull + @Override + public DocStringBuilder createDocString() { + return new TagBasedDocStringBuilder(TAG_PREFIX); + } +} diff --git a/python/src/com/jetbrains/python/documentation/docstrings/SphinxDocstringProvider.java b/python/src/com/jetbrains/python/documentation/docstrings/SphinxDocstringProvider.java new file mode 100644 index 000000000000..e1d238fcca85 --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/docstrings/SphinxDocstringProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2015 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.documentation.SphinxDocString; +import com.jetbrains.python.toolbox.Substring; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class SphinxDocstringProvider extends DocStringProvider{ + + public static final String TAG_PREFIX = ":"; + + @Override + public SphinxDocString parseDocString(@NotNull Substring content) { + return new SphinxDocString(content); + } + + @NotNull + @Override + public DocStringUpdater updateDocString(@NotNull SphinxDocString docstring) { + return new TagBasedDocStringUpdater(docstring, TAG_PREFIX) { + @Override + public DocStringBuilder createDocStringBuilder() { + return createDocString(); + } + }; + } + + @NotNull + @Override + public DocStringBuilder createDocString() { + return new TagBasedDocStringBuilder(TAG_PREFIX); + } +} diff --git a/python/src/com/jetbrains/python/documentation/docstrings/TagBasedDocStringBuilder.java b/python/src/com/jetbrains/python/documentation/docstrings/TagBasedDocStringBuilder.java new file mode 100644 index 000000000000..5c350f9babda --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/docstrings/TagBasedDocStringBuilder.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2015 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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class TagBasedDocStringBuilder extends DocStringBuilder { + private final String myTagPrefix; + + public TagBasedDocStringBuilder(@NotNull String prefix) { + myTagPrefix = prefix; + } + + @NotNull + @Override + public DocStringBuilder addParameter(@NotNull String name, @Nullable String type) { + addLine(String.format("%sparam %s: ", myTagPrefix, name)); + if (type != null) { + addParameterType(name, type); + } + return this; + } + + @Override + public DocStringBuilder addParameterType(@NotNull String name, @NotNull String type) { + addLine(String.format("%stype %s: ", myTagPrefix, type)); + return this; + } + + @NotNull + @Override + public DocStringBuilder addReturnValue(@Nullable String name, @NotNull String type) { + // named return values are not supported in Sphinx and Epydoc + addLine(String.format("%srtype: %s", myTagPrefix, type)); + return this; + } +} diff --git a/python/src/com/jetbrains/python/documentation/docstrings/TagBasedDocStringUpdater.java b/python/src/com/jetbrains/python/documentation/docstrings/TagBasedDocStringUpdater.java new file mode 100644 index 000000000000..ff2239e4e9f9 --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/docstrings/TagBasedDocStringUpdater.java @@ -0,0 +1,89 @@ +/* + * Copyright 2000-2015 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.documentation.TagBasedDocString; +import com.jetbrains.python.toolbox.Substring; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public abstract class TagBasedDocStringUpdater extends DocStringUpdater{ + + private final String myTagPrefix; + + public TagBasedDocStringUpdater(@NotNull T docString, @NotNull String prefix) { + super(docString); + myTagPrefix = prefix; + } + + @Override + protected void scheduleUpdates() { + final int anchorLine = firstLineWithTag(); + final int anchorLineIndent = myOriginalDocString.getLineIndent(anchorLine); + for (AddParameter paramReq : myAddParameterRequests) { + if (!myOriginalDocString.getParameters().contains(paramReq.name)) { + insertAfterLine(anchorLine, createParameterNameLine(paramReq.name, anchorLineIndent)); + } + if (paramReq.type != null) { + final Substring typeSub = myOriginalDocString.getParamTypeSubstring(paramReq.name); + if (typeSub != null) { + replace(typeSub.getTextRange(), paramReq.type); + } + else { + insertAfterLine(anchorLine, createParameterTypeLine(paramReq.name, paramReq.type, anchorLineIndent)); + } + } + } + for (AddReturnType returnReq : myAddReturnTypeRequests) { + final Substring typeSub = myOriginalDocString.getReturnTypeSubstring(); + if (typeSub != null) { + replace(typeSub.getTextRange(), returnReq.type); + } + else { + insertAfterLine(anchorLine, createReturnTypeLine(returnReq.type, anchorLineIndent)); + } + } + } + + @NotNull + private String createParameterNameLine(@NotNull String name, int indent) { + return createDocStringBuilder().addParameter(name, null).buildContent(indent, true); + } + + @NotNull + private String createParameterTypeLine(@NotNull String name, @NotNull String type, int indent) { + return createDocStringBuilder().addParameterType(name, type).buildContent(indent, true); + } + + @NotNull + private String createReturnTypeLine(@NotNull String type, int indent) { + return createDocStringBuilder().addReturnValue(null, type).buildContent(indent, true); + } + + private int firstLineWithTag() { + for (int i = 0; i < myOriginalDocString.getLineCount(); i++) { + final Substring line = myOriginalDocString.getLine(i); + if (line.contains(myTagPrefix)) { + return i; + } + } + return myOriginalDocString.getLineCount() - 1; + } + + public abstract DocStringBuilder createDocStringBuilder(); +} diff --git a/python/testSrc/com/jetbrains/python/EpydocStringTest.java b/python/testSrc/com/jetbrains/python/EpydocStringTest.java index 324b16071024..c9d0b87df815 100644 --- a/python/testSrc/com/jetbrains/python/EpydocStringTest.java +++ b/python/testSrc/com/jetbrains/python/EpydocStringTest.java @@ -18,6 +18,7 @@ package com.jetbrains.python; import com.intellij.testFramework.UsefulTestCase; import com.jetbrains.python.documentation.EpydocString; import com.jetbrains.python.toolbox.Substring; +import org.jetbrains.annotations.NotNull; import java.util.List; @@ -26,22 +27,27 @@ import java.util.List; */ public class EpydocStringTest extends UsefulTestCase { public void testTagValue() { - EpydocString docString = new EpydocString("@rtype: C{str}"); + EpydocString docString = createEpydocDocString("@rtype: C{str}"); Substring s = docString.getTagValue("rtype"); assertNotNull(s); assertEquals("C{str}", s.toString()); } + @NotNull + private EpydocString createEpydocDocString(String s) { + return new EpydocString(new Substring(s)); + } + public void testTagWithParamValue() { - EpydocString docString = new EpydocString("@type m: number"); + EpydocString docString = createEpydocDocString("@type m: number"); final Substring s = docString.getTagValue("type", "m"); assertNotNull(s); assertEquals("number", s.toString()); } public void testMultilineTag() { - EpydocString docString = new EpydocString(" @param b: The y intercept of the line. The X{y intercept} of a\n" + - " line is the point at which it crosses the y axis (M{x=0})."); + EpydocString docString = createEpydocDocString(" @param b: The y intercept of the line. The X{y intercept} of a\n" + + " line is the point at which it crosses the y axis (M{x=0})."); final Substring s = docString.getTagValue("param", "b"); assertNotNull(s); assertEquals("The y intercept of the line. The X{y intercept} of a line is the point at which it crosses the y axis (M{x=0}).", @@ -56,24 +62,24 @@ public class EpydocStringTest extends UsefulTestCase { } public void testMultipleTags() { - EpydocString docString = new EpydocString(" \"\"\"\n" + - " Run the given function wrapped with seteuid/setegid calls.\n" + - "\n" + - " This will try to minimize the number of seteuid/setegid calls, comparing\n" + - " current and wanted permissions\n" + - "\n" + - " @param euid: effective UID used to call the function.\n" + - " @type euid: C{int}\n" + - "\n" + - " @param egid: effective GID used to call the function.\n" + - " @type egid: C{int}\n" + - "\n" + - " @param function: the function run with the specific permission.\n" + - " @type function: any callable\n" + - "\n" + - " @param *args: arguments passed to function\n" + - " @param **kwargs: keyword arguments passed to C{function}\n" + - " \"\"\""); + EpydocString docString = createEpydocDocString(" \"\"\"\n" + + " Run the given function wrapped with seteuid/setegid calls.\n" + + "\n" + + " This will try to minimize the number of seteuid/setegid calls, comparing\n" + + " current and wanted permissions\n" + + "\n" + + " @param euid: effective UID used to call the function.\n" + + " @type euid: C{int}\n" + + "\n" + + " @param egid: effective GID used to call the function.\n" + + " @type egid: C{int}\n" + + "\n" + + " @param function: the function run with the specific permission.\n" + + " @type function: any callable\n" + + "\n" + + " @param *args: arguments passed to function\n" + + " @param **kwargs: keyword arguments passed to C{function}\n" + + " \"\"\""); final List params = docString.getParameters(); assertOrderedEquals(params, "euid", "egid", "function", "*args", "**kwargs");