PY-29717 Remove excess trailing spaces and line breaks from plain text docstrings

This commit is contained in:
Mikhail Golubev
2018-05-31 15:06:23 +03:00
parent 4bcb3778cd
commit 505efb9982
12 changed files with 56 additions and 73 deletions

View File

@@ -15,21 +15,16 @@
*/
package com.jetbrains.python.documentation;
import com.intellij.application.options.CodeStyle;
import com.intellij.lang.ASTNode;
import com.intellij.lang.documentation.DocumentationMarkup;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.FactoryMap;
import com.jetbrains.python.*;
@@ -42,25 +37,23 @@ import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.QualifiedNameFinder;
import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
import com.jetbrains.python.psi.resolve.RootVisitor;
import com.jetbrains.python.psi.types.PyClassType;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.pyi.PyiUtil;
import com.jetbrains.python.toolbox.ChainIterable;
import com.jetbrains.python.toolbox.Maybe;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.jetbrains.python.documentation.DocumentationBuilderKit.*;
@@ -327,11 +320,10 @@ public class PyDocumentationBuilder {
if (!isProperty) {
pyClass = pyFunction.getContainingClass();
if (pyClass != null) {
myBody
.addWith(TagSmall,
PythonDocumentationProvider.describeClass(pyClass, Function.identity(), TO_ONE_LINE_AND_ESCAPE, true, true, myContext))
.addItem(BR)
.addItem(BR);
final String link = getLinkToClass(pyClass);
if (link != null) {
myProlog.addItem(link);
}
}
}
myBody.add(PythonDocumentationProvider.describeDecorators(pyFunction, WRAP_IN_ITALIC, ESCAPE_AND_SAVE_NEW_LINES_AND_SPACES, BR, BR));
@@ -430,14 +422,9 @@ public class PyDocumentationBuilder {
if (pyClass == ancestor) {
ancestorLink = PyDocumentationLink.toContainingClass(ancestorName);
}
else if (ancestorQualifiedName != null && ancestorName != null) {
ancestorLink = PyDocumentationLink.toPossibleClass(ancestorName, ancestorQualifiedName, pyClass, myContext);
}
else {
// TODO add a way to reference other local classes
ancestorLink = ancestorName;
ancestorLink = getLinkToClass(ancestor);
}
}
if (ancestorLink != null) {
mySectionsMap.get(PyBundle.message("QDOC.documentation.is.copied.from")).addWith(TagCode, $(ancestorLink));
@@ -474,21 +461,21 @@ public class PyDocumentationBuilder {
@NotNull
private static ChainIterable<String> formatDocString(@NotNull PsiElement element, @NotNull String docstring) {
final Project project = element.getProject();
final List<String> formatted = PyStructuredDocstringFormatter.formatDocstring(element, docstring);
if (formatted != null) {
return new ChainIterable<>(formatted);
}
boolean isFirstLine;
final String[] lines = removeCommonIndentation(docstring);
final List<String> origLines = LineTokenizer.tokenizeIntoList(docstring.trim(), false, false);
final List<String> updatedLines = StreamEx.of(PyIndentUtil.removeCommonIndent(origLines, true))
.takeWhile(line -> !line.startsWith(PyConsoleUtil.ORDINARY_PROMPT))
.toList();
final ChainIterable<String> result = new ChainIterable<>();
// reconstruct back, dropping first empty fragment as needed
isFirstLine = true;
final int tabSize = CodeStyleSettingsManager.getSettings(project).getTabSize(PythonFileType.INSTANCE);
for (String line : lines) {
boolean isFirstLine = true;
final int tabSize = CodeStyle.getIndentOptions(element.getContainingFile()).TAB_SIZE;
for (String line : updatedLines) {
if (isFirstLine && ourSpacesPattern.matcher(line).matches()) continue; // ignore all initial whitespace
if (isFirstLine) {
isFirstLine = false;
@@ -508,44 +495,6 @@ public class PyDocumentationBuilder {
return result;
}
public static String[] removeCommonIndentation(@NotNull final String docstring) {
// detect common indentation
final String[] lines = LineTokenizer.tokenize(docstring, false);
boolean isFirst = true;
int cutWidth = Integer.MAX_VALUE;
int firstIndentedLine = 0;
for (String frag : lines) {
if (frag.length() == 0) continue;
int padWidth = 0;
final Matcher matcher = ourSpacesPattern.matcher(frag);
if (matcher.find()) {
padWidth = matcher.end();
}
if (isFirst) {
isFirst = false;
if (padWidth == 0) { // first line may have zero padding
firstIndentedLine = 1;
continue;
}
}
if (padWidth < cutWidth) cutWidth = padWidth;
}
// remove common indentation
if (cutWidth > 0 && cutWidth < Integer.MAX_VALUE) {
for (int i = firstIndentedLine; i < lines.length; i += 1) {
if (lines[i].length() >= cutWidth) {
lines[i] = lines[i].substring(cutWidth);
}
}
}
final List<String> result = new ArrayList<>();
for (String line : lines) {
if (line.startsWith(PyConsoleUtil.ORDINARY_PROMPT)) break;
result.add(line);
}
return ArrayUtil.toStringArray(result);
}
private void addModulePath(@NotNull PyFile followed) {
// what to prepend to a module description?
final VirtualFile file = followed.getVirtualFile();
@@ -565,6 +514,15 @@ public class PyDocumentationBuilder {
}
}
@Nullable
private String getLinkToClass(@NotNull PyClass pyClass) {
final String qualifiedName = pyClass.getQualifiedName();
if (qualifiedName != null && pyClass.getName() != null) {
return PyDocumentationLink.toPossibleClass(pyClass.getName(), qualifiedName, pyClass, myContext);
}
return pyClass.getName();
}
@Nullable
static PyStringLiteralExpression getEffectiveDocStringExpression(@NotNull PyDocStringOwner owner) {
final PyStringLiteralExpression expression = owner.getDocStringExpression();

View File

@@ -1 +1 @@
<html><body><div class='definition'><pre>def&nbsp;<b>len</b>(o:&nbsp;Sized)&nbsp;-&gt;&nbsp;<a href="psi_element://#typename#int">int</a></pre></div><div class='content'><br>len(object)&nbsp;-&gt;&nbsp;integer<br><br>Return&nbsp;the&nbsp;number&nbsp;of&nbsp;items&nbsp;of&nbsp;a&nbsp;sequence&nbsp;or&nbsp;collection.<br></div></body></html>
<html><body><div class='definition'><pre>def&nbsp;<b>len</b>(o:&nbsp;Sized)&nbsp;-&gt;&nbsp;<a href="psi_element://#typename#int">int</a></pre></div><div class='content'>len(object)&nbsp;-&gt;&nbsp;integer<br><br>Return&nbsp;the&nbsp;number&nbsp;of&nbsp;items&nbsp;of&nbsp;a&nbsp;sequence&nbsp;or&nbsp;collection.</div></body></html>

View File

@@ -1 +1 @@
<html><body><div class='definition'><pre>Class attribute <b><code>the_attr</code></b> of class <a href="psi_element://#class#"><code>C</code></a><br>the_attr: <a href="psi_element://#typename#str">str</a></pre></div><div class='content'>The&nbsp;documentation&nbsp;for&nbsp;the&nbsp;attribute.&nbsp;</div></body></html>
<html><body><div class='definition'><pre>Class attribute <b><code>the_attr</code></b> of class <a href="psi_element://#class#"><code>C</code></a><br>the_attr: <a href="psi_element://#typename#str">str</a> = &quot;&quot;</pre></div><div class='content'>The&nbsp;documentation&nbsp;for&nbsp;the&nbsp;attribute.</div></body></html>

View File

@@ -1 +1 @@
<html><body><div class='definition'><pre>Instance attribute <b><code>foo</code></b> of class <a href="psi_element://#class#"><code>C</code></a><br>foo: <a href="psi_element://#typename#str">str</a></pre></div><div class='content'>The&nbsp;docstring&nbsp;for&nbsp;the&nbsp;attribute&nbsp;foo.&nbsp;</div></body></html>
<html><body><div class='definition'><pre>Instance attribute <b><code>foo</code></b> of class <a href="psi_element://#class#"><code>C</code></a><br>foo: <a href="psi_element://#typename#str">str</a> = &quot;Foo&quot;</pre></div><div class='content'>The&nbsp;docstring&nbsp;for&nbsp;the&nbsp;attribute&nbsp;foo.</div></body></html>

View File

@@ -1 +1 @@
<html><body><div class='definition'><pre><small>class <a href="psi_element://#class#">Foo</a></small><br><br>@<i>deco</i><br>def&nbsp;<b>meth</b>(self:&nbsp;<a href="psi_element://#typename#Foo">Foo</a>)&nbsp;-&gt;&nbsp;Optional[Any]</pre></div><div class='content'><br>Doc&nbsp;of&nbsp;meth.<br></div></body></html>
<html><body><div class='definition'><pre><small>class <a href="psi_element://#class#">Foo</a></small><br><br>@<i>deco</i><br>def&nbsp;<b>meth</b>(self:&nbsp;<a href="psi_element://#typename#Foo">Foo</a>)&nbsp;-&gt;&nbsp;Optional[Any]</pre></div><div class='content'>Doc&nbsp;of&nbsp;meth.</div></body></html>

View File

@@ -1 +1 @@
<html><body><div class='definition'><pre>Module <b>Module</b></pre></div><div class='content'><br>Module's&nbsp;doc.</div></body></html>
<html><body><div class='definition'><pre>Module <b>Module</b></pre></div><div class='content'>Module's&nbsp;doc.</div></body></html>

View File

@@ -0,0 +1 @@
<html><body><div class='definition'><pre>def&nbsp;<b>func</b>()&nbsp;-&gt;&nbsp;None</pre></div><div class='content'>Docstring.</div></body></html>

View File

@@ -0,0 +1,8 @@
def fu<ref1>nc():
"""Docstring."""
def fu<ref2>nc():
"""
Docstring.
"""

View File

@@ -1 +1 @@
<html><body><div class='definition'><pre>property <b><code>m</code></b> of class <a href="psi_element://#class#">C</a><br>@<i>m.setter</i><br>def&nbsp;<b>m</b>(self:&nbsp;<a href="psi_element://#typename#C">C</a>,&nbsp;x:&nbsp;Any)&nbsp;-&gt;&nbsp;None</pre></div><div class='content'><br>Foo<br></div><table class='sections'><tr><td valign='top' class='section'><p>Documentation is copied from:</td><td valign='top'>property getter</td><tr><td valign='top' class='section'><p>Accessor kind:</td><td valign='top'>Setter</td></table></body></html>
<html><body><div class='definition'><pre>property <b><code>m</code></b> of class <a href="psi_element://#class#">C</a><br>@<i>m.setter</i><br>def&nbsp;<b>m</b>(self:&nbsp;<a href="psi_element://#typename#C">C</a>,&nbsp;x:&nbsp;Any)&nbsp;-&gt;&nbsp;None</pre></div><div class='content'>Foo</div><table class='sections'><tr><td valign='top' class='section'><p>Documentation is copied from:</td><td valign='top'>property getter</td><tr><td valign='top' class='section'><p>Accessor kind:</td><td valign='top'>Setter</td></table></body></html>

View File

@@ -1 +1 @@
<html><body><div class='definition'><pre><small>class <a href="psi_element://#class#">list</a>(<a href="psi_element://#typename#typing.MutableSequence">MutableSequence</a>[_T], Generic[_T])</small><br><br>def&nbsp;<b>count</b>(self:&nbsp;<a href="psi_element://#typename#list">list</a>,&nbsp;object:&nbsp;_T)&nbsp;-&gt;&nbsp;<a href="psi_element://#typename#int">int</a></pre></div><div class='content'>L.count(value)&nbsp;-&gt;&nbsp;integer&nbsp;--&nbsp;return&nbsp;number&nbsp;of&nbsp;occurrences&nbsp;of&nbsp;value&nbsp;</div><table class='sections'><tr><td valign='top' class='section'><p>Assigned to:</td><td valign='top'><code>c1</code></td></table></body></html>
<html><body><div class='definition'><pre><small>class <a href="psi_element://#class#">list</a>(<a href="psi_element://#typename#typing.MutableSequence">MutableSequence</a>[_T], Generic[_T])</small><br><br>def&nbsp;<b>count</b>(self:&nbsp;<a href="psi_element://#typename#list">list</a>,&nbsp;object:&nbsp;_T)&nbsp;-&gt;&nbsp;<a href="psi_element://#typename#int">int</a></pre></div><div class='content'>L.count(value)&nbsp;-&gt;&nbsp;integer&nbsp;--&nbsp;return&nbsp;number&nbsp;of&nbsp;occurrences&nbsp;of&nbsp;value</div><table class='sections'><tr><td valign='top' class='section'><p>Assigned to:</td><td valign='top'><code>c1</code></td></table></body></html>

View File

@@ -1 +1 @@
<html><body><div class='definition'><pre>y: <a href="psi_element://#typename#int">int</a></pre></div></body></html>
<html><body><div class='definition'><pre>y: <a href="psi_element://#typename#int">int</a> = 1</pre></div></body></html>

View File

@@ -389,4 +389,20 @@ public class PyQuickDocTest extends LightMarkedTestCase {
}
);
}
public void testPlainTextDocstringsQuotesPlacementDoesntAffectFormatting() {
runWithDocStringFormat(DocStringFormat.PLAIN, () -> {
final Map<String, PsiElement> map = loadTest();
final PsiElement inline = map.get("<ref1>");
final String inlineDoc = myProvider.generateDoc(assertInstanceOf(inline.getParent(), PyFunction.class), inline);
final PsiElement framed = map.get("<ref2>");
final String framedDoc = myProvider.generateDoc(assertInstanceOf(framed.getParent(), PyFunction.class), framed);
assertEquals(inlineDoc, framedDoc);
checkByHTML(inlineDoc);
});
}
}