diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties
index b6a04f90fa74..09954c8ba495 100644
--- a/python/src/com/jetbrains/python/PyBundle.properties
+++ b/python/src/com/jetbrains/python/PyBundle.properties
@@ -177,6 +177,8 @@ PARSE.expected.formal.param.name=formal parameter name expected
### qiuck doc generator
QDOC.copied.from.$0.$1=Documentation is missing. The following if copied from {0}.{1}.
QDOC.copied.from.builtin=(copied from built-in description)
+QDOC.assigned.to.$0=Assigned to {0}
+QDOC.wrapped.in.$0=Wrapped in {0}
### unittest run configuration
runcfg.unittest.display_name=Python's unittest
diff --git a/python/src/com/jetbrains/python/PythonDocumentationProvider.java b/python/src/com/jetbrains/python/PythonDocumentationProvider.java
index af54657d7fee..c17bfb1dbfcb 100644
--- a/python/src/com/jetbrains/python/PythonDocumentationProvider.java
+++ b/python/src/com/jetbrains/python/PythonDocumentationProvider.java
@@ -8,12 +8,15 @@ import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyCallExpressionHelper;
import com.jetbrains.python.psi.types.PyClassType;
-import com.jetbrains.python.toolbox.FP;
+import com.jetbrains.python.toolbox.*;
import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
-import java.util.EnumSet;
+import java.util.LinkedList;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* Provides quick docs for classes, methods, and functions.
@@ -31,11 +34,11 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
cat.append("class ").append(cls_name).append("\n ");
// It would be nice to have class import info here, but we don't know the ctrl+hovered reference and context
}
- return describeFunction(func, cat, LSame2, ", ", LSame2, LSame1).toString();
+ return describeFunction(func, LSame2, ", ", LSame2, LSame1).toString();
}
else if (element instanceof PyClass) {
PyClass cls = (PyClass)element;
- return describeClass(cls, new StringBuilder(), LSame2).toString();
+ return describeClass(cls, LSame2).toString();
}
return null;
}
@@ -56,23 +59,22 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
* @param escaper sanitizes values that come directly from doc string or code
* @return cat for easy chaining
*/
- private static StringBuilder describeFunction(
+ private static ChainIterable describeFunction(
PyFunction fun,
- StringBuilder cat,
- FP.Lambda2 deco_name_wrapper,
+ FP.Lambda1, Iterable> deco_name_wrapper,
String deco_separator,
- FP.Lambda2 func_name_wrapper,
+ FP.Lambda1, Iterable> func_name_wrapper,
FP.Lambda1 escaper
) {
+ ChainIterable cat = new ChainIterable(null);
PyDecoratorList deco_list = fun.getDecoratorList();
if (deco_list != null) {
for (PyDecorator deco : deco_list.getDecorators()) {
- describeDeco(deco, cat, deco_name_wrapper, escaper).append(deco_separator);
+ cat.add(describeDeco(deco, deco_name_wrapper, escaper)).add(deco_separator); // can't easily pass describeDeco to map() %)
}
}
- cat.append("def "); //.append("").append(fun.getName()).append("");
- func_name_wrapper.apply(fun.getName(), cat);
- cat.append(escaper.apply(PyUtil.getReadableRepr(fun.getParameterList(), false)));
+ cat.add("def ").addWith(func_name_wrapper, $(fun.getName()));
+ cat.add(escaper.apply(PyUtil.getReadableRepr(fun.getParameterList(), false)));
return cat;
}
@@ -82,44 +84,108 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
* @param cat string buffer to append to
* @return cat for easy chaining
*/
- private static StringBuilder describeClass(
+ private static ChainIterable describeClass(
PyClass cls,
- StringBuilder cat,
- FP.Lambda2 name_wrapper
+ FP.Lambda1, Iterable> name_wrapper
) {
- cat.append("class ");
- name_wrapper.apply(cls.getName(), cat);
+ ChainIterable cat = new ChainIterable();
+ cat.add("class ").addWith(name_wrapper, $(cls.getName()));
final PyExpression[] ancestors = cls.getSuperClassExpressions();
if (ancestors.length > 0) {
- cat.append("(");
- join(", ", FP.map(LReadableRepr, Arrays.asList(ancestors)), cat);
- cat.append(")");
+ cat.add("(").add(interleave(FP.map(LReadableRepr, ancestors), ", ")).add(")");
}
// TODO: for py3k, show decorators
return cat;
}
+ //
+ private static Iterable describeDeco(
+ PyDecorator deco,
+ final FP.Lambda1, Iterable> name_wrapper, // addWith in tags, if need be
+ final FP.Lambda1 arg_wrapper // add escaping, if need be
+ ) {
+ ChainIterable cat = new ChainIterable();
+ cat.add("@").addWith(name_wrapper, $(PyUtil.getReadableRepr(deco.getCallee(), true)));
+ if (deco.hasArgumentList()) {
+ PyArgumentList arglist = deco.getArgumentList();
+ if (arglist != null) {
+ cat
+ .add("(")
+ .add(interleave(FP.map(FP.combine(LReadableRepr, arg_wrapper), arglist.getArguments()), ", "))
+ .add(")")
+ ;
+ }
+ }
+ return cat;
+ }
+
+ private static @NotNull ChainIterable combUpDocString(@NotNull String docstring) {
+ ChainIterable cat = new ChainIterable();
+ // detect common indentation
+ String[] fragments = Pattern.compile("\n").split(docstring);
+ Pattern spaces_pat = Pattern.compile("^\\s+");
+ boolean is_first = true;
+ final int IMPOSSIBLY_BIG = 999999;
+ int cut_width = IMPOSSIBLY_BIG;
+ for (String frag : fragments) {
+ if (frag.length() == 0) continue;
+ int pad_width = 0;
+ final Matcher matcher = spaces_pat.matcher(frag);
+ if (matcher.find()) {
+ pad_width = matcher.end();
+ if (is_first) {
+ is_first = false;
+ if (pad_width == 0) continue; // first line may have zero padding // first line may have zero padding
+ }
+ }
+ if (pad_width < cut_width) cut_width = pad_width;
+ }
+ // remove common indentation
+ if (cut_width > 0 && cut_width < IMPOSSIBLY_BIG) {
+ for (int i=0; i < fragments.length; i+= 1) {
+ if (fragments[i].length() > 0) fragments[i] = fragments[i].substring(cut_width);
+ }
+ }
+ // reconstruct back, dropping first empty fragment as needed
+ is_first = true;
+ for (String frag : fragments) {
+ if (is_first && spaces_pat.matcher(frag).matches()) continue; // ignore all initial whitespace
+ if (is_first) is_first = false;
+ else cat.add(BR);
+ cat.add(combUp(frag));
+ }
+ return cat;
+ }
// provides ctrl+Q doc
public String generateDoc(PsiElement element, final PsiElement originalElement) {
- final StringBuilder cat = new StringBuilder("");
+ ChainIterable cat = new ChainIterable(); // our main output sequence
+ final ChainIterable prolog_cat = new ChainIterable(); // sequence for reassignment info, etc
+ final ChainIterable doc_cat = new ChainIterable(); // sequence for doc string
+ final ChainIterable epilog_cat = new ChainIterable(); // sequence for doc "copied from" notices and such
+
+ cat.add(prolog_cat).addWith(TagCode, doc_cat).add(epilog_cat); // pre-assemble; then add stuff to individual cats as needed
+ cat = wrapInTag("html", wrapInTag("body", cat));
+
// here the ^Q target is already resolved; the resolved element may point to intermediate assignments
boolean reassignment_marked = false;
if (element instanceof PyTargetExpression) {
if (! reassignment_marked) {
- LWrapInSmall.enclose(cat, "Assigned to ", element.getText(), BR);
+ //prolog_cat.add(TagSmall.apply($("Assigned to ", element.getText(), BR)));
+ prolog_cat.addWith(TagSmall, $(PyBundle.message("QDOC.assigned.to.$0", element.getText())).add(BR));
reassignment_marked = true;
}
element = PyUtil.findAssignedValue((PyTargetExpression)element);
}
if (element instanceof PyReferenceExpression) {
if (! reassignment_marked) {
- LWrapInSmall.enclose(cat, "Assigned to ", element.getText(), BR);
+ //prolog_cat.add(TagSmall.apply($("Assigned to ", element.getText(), BR)));
+ prolog_cat.addWith(TagSmall, $(PyBundle.message("QDOC.assigned.to.$0", element.getText())).add(BR));
reassignment_marked = true;
}
element = PyUtil.followAssignmentsChain((PyReferenceExpression)element);
}
- // it may be a call to decorator
+ // it may be a call to a standard wrapper
if (element instanceof PyCallExpression) {
Pair wrap_info = PyCallExpressionHelper.interpretAsStaticmethodOrClassmethodWrappingCall(
(PyCallExpression)element, originalElement
@@ -127,34 +193,32 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
if (wrap_info != null) {
String wrapper_name = wrap_info.getFirst();
PyFunction wrapped_func = wrap_info.getSecond();
- LWrapInSmall.enclose(cat, "Wrapped in ", wrapper_name, "", BR); // NOTE: abstraction fail :(
+ //prolog_cat.addWith(TagSmall, $("Wrapped in ").addWith(TagCode, $(wrapper_name)).add(BR));
+ prolog_cat.addWith(TagSmall, $(PyBundle.message("QDOC.wrapped.in.$0", wrapper_name)).add(BR));
element = wrapped_func;
}
}
// now element may contain a doc string
- cat.append("");
if (element instanceof PyDocStringOwner) {
String docString = null;
- boolean prepended_something = false;
PyStringLiteralExpression doc_expr = ((PyDocStringOwner) element).getDocStringExpression();
if (doc_expr != null) docString = doc_expr.getStringValue();
+ // doc of what?
if (element instanceof PyClass) {
PyClass cls = (PyClass)element;
- describeClass(cls, cat, LWrapInBold);
- prepended_something = true;
+ doc_cat.addWith(TagSmall, describeClass(cls, TagBold));
}
else if (element instanceof PyFunction) {
PyFunction fun = (PyFunction)element;
PyClass cls = fun.getContainingClass();
- if (cls != null) LWrapInSmall.enclose(cat, "class ", cls.getName(), BR);
- describeFunction(fun, cat, LWrapInItalic, BR, LWrapInBold, LCombUp);
- prepended_something = true;
+ if (cls != null) doc_cat.addWith(TagSmall, $("class ", cls.getName(), BR));
+ doc_cat.add(describeFunction(fun, TagItalic, BR, TagBold, LCombUp));
boolean not_found = true;
if (docString == null) {
String meth_name = fun.getName();
if (cls != null && meth_name != null ) {
- // look for inherited
+ // look for inherited and its doc
for (PyClass ancestor : cls.iterateAncestors()) {
PyFunction inherited = ancestor.findMethodByName(meth_name, false);
if (inherited != null) {
@@ -162,12 +226,11 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
if (doc_elt != null) {
String inherited_doc = doc_elt.getStringValue();
if (inherited_doc.length() > 1) {
- cat
- .append(BR).append(BR).append("")
- .append(PyBundle.message("QDOC.copied.from.$0.$1", ancestor.getName(), meth_name))
- .append(BR).append(BR)
- .append(inherited_doc)
- .append("")
+ epilog_cat
+ .add(BR).add(BR)
+ .add(PyBundle.message("QDOC.copied.from.$0.$1", ancestor.getName(), meth_name))
+ .add(BR).add(BR)
+ .addWith(TagCode, $(inherited_doc))
;
not_found = false;
break;
@@ -190,13 +253,8 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
PyStringLiteralExpression predefined_doc_expr = obj_underscored.getDocStringExpression();
String predefined_doc = predefined_doc_expr != null? predefined_doc_expr.getStringValue() : null;
if (predefined_doc != null && predefined_doc.length() > 1) { // only a real-looking doc string counts
- cat
- .append(BR).append(BR).append("")
- .append(predefined_doc)
- .append(BR)
- .append(PyBundle.message("QDOC.copied.from.builtin"))
- .append("")
- ;
+ doc_cat.add(predefined_doc);
+ epilog_cat.add(BR).add(BR).add(PyBundle.message("QDOC.copied.from.builtin"));
}
}
}
@@ -208,49 +266,20 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
}
else if (element instanceof PyFile) {
// what to prepend to a module description??
+ prolog_cat.add("blah!");
}
else { // not a func, not a class
- cat.append(combUp(PyUtil.getReadableRepr(element, false)));
- prepended_something = true;
+ doc_cat.add(combUp(PyUtil.getReadableRepr(element, false)));
}
- cat.append("");
if (docString != null) {
- if (prepended_something) cat.append(BR).append(BR);
- cat.append(combUp(docString.trim()));
+ doc_cat.add(BR).add(combUpDocString(docString));
}
- else if (! prepended_something) return null; // no doc, no prepend -> not found
- return cat.append("").toString();
+ else if (prolog_cat.isEmpty() && doc_cat.isEmpty() && epilog_cat.isEmpty()) return null; // got nothing to say!
+ return cat.toString();
}
return null;
}
- // // this is a shaky mirage of another world. instead, gotta learn how to write in Jython here.
-
- //
- private static StringBuilder describeDeco(
- PyDecorator deco,
- final StringBuilder cat,
- final FP.Lambda2 name_wrapper, // wrap in tags, if need be
- final FP.Lambda1 arg_wrapper // add escaping, if need be
- ) {
- cat.append("@");
- name_wrapper.apply(PyUtil.getReadableRepr(deco.getCallee(), true), cat);
- if (deco.hasArgumentList()) {
- PyArgumentList arglist = deco.getArgumentList();
- if (arglist != null) {
- List argnames = FP.map(
- FP.combine(LReadableRepr, arg_wrapper),
- Arrays.asList(arglist.getArguments())
- );
-
- cat.append("(");
- join(", ", argnames, cat);
- cat.append(")");
- }
- }
- return cat;
- }
-
private static FP.Lambda1 LCombUp = new FP.Lambda1() {
public String apply(String argname) {
return combUp(argname);
@@ -263,33 +292,38 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
}
};
- private static class TagWrapper implements FP.Lambda2 {
- private String start_tag, end_tag;
- TagWrapper(String tag) {
- end_tag = "" + tag + ">";
- start_tag = "<" + tag + ">";
- }
-
- public StringBuilder apply(String content, @NonNls StringBuilder cat) {
- return cat.append(start_tag).append(content).append(end_tag);
- }
-
- public StringBuilder enclose(@NonNls StringBuilder cat, String... contents) {
- cat.append(start_tag);
- for (String item : contents) cat.append(item);
- cat.append(end_tag);
- return cat;
- }
+ private static ChainIterable wrapInTag(String tag, Iterable content) {
+ return new ChainIterable(new SingleIterable("<" + tag + ">")).add(content).add(new SingleIterable("" + tag + ">"));
}
- private static TagWrapper LWrapInBold = new TagWrapper("b");
- private static TagWrapper LWrapInItalic = new TagWrapper("i");
- private static TagWrapper LWrapInSmall = new TagWrapper("small");
+ private static ChainIterable $(String... content) {
+ return new ChainIterable(Arrays.asList(content));
+ }
- private static FP.Lambda2 LSame2 = new FP.Lambda2() {
- public StringBuilder apply(String name, StringBuilder cat) {
- return cat.append(name);
+
+ // make a first-order curried objects out of wrapInTag()
+ private static class TagWrapper implements FP.Lambda1, Iterable> {
+ private String myTag;
+
+ TagWrapper(String tag) {
+ myTag = tag;
+ }
+
+ public Iterable apply(Iterable contents) {
+ return wrapInTag(myTag, contents);
+ }
+
+ }
+
+ private static TagWrapper TagBold = new TagWrapper("b");
+ private static TagWrapper TagItalic = new TagWrapper("i");
+ private static TagWrapper TagSmall = new TagWrapper("small");
+ private static TagWrapper TagCode = new TagWrapper("code");
+
+ private static FP.Lambda1, Iterable> LSame2 = new FP.Lambda1, Iterable>() {
+ public Iterable apply(Iterable what) {
+ return what;
}
};
@@ -299,7 +333,8 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
}
};
- private static StringBuilder join(String delimiter, List list, StringBuilder cat) {
+ /*
+ private static StringBuilder join(String delimiter, Iterable list, StringBuilder cat) {
boolean is_next = false;
for (Object item : list) {
if (is_next) cat.append(delimiter);
@@ -312,4 +347,16 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider {
private static String join(String delimiter, List list) {
return join(delimiter, list, new StringBuilder()).toString();
}
+ */
+
+ private static Iterable interleave(Iterable source, T filler) {
+ List ret = new LinkedList();
+ boolean is_next = false;
+ for (T what : source) {
+ if (is_next) ret.add(filler);
+ else is_next = true;
+ ret.add(what);
+ }
+ return ret;
+ }
}
diff --git a/python/testData/quickdoc/CallFunc.html b/python/testData/quickdoc/CallFunc.html
index 5f16209f2442..77d22808f803 100644
--- a/python/testData/quickdoc/CallFunc.html
+++ b/python/testData/quickdoc/CallFunc.html
@@ -1,2 +1 @@
-def foo()
Doc of foo.
-
+def foo()
Doc of foo.
diff --git a/python/testData/quickdoc/ClassConstructor.html b/python/testData/quickdoc/ClassConstructor.html
index 28d6d74611d3..8991500da1d8 100644
--- a/python/testData/quickdoc/ClassConstructor.html
+++ b/python/testData/quickdoc/ClassConstructor.html
@@ -1,2 +1 @@
-class Foo(object)
Doc of Foo.
-
+class Foo(object)
Doc of Foo.
diff --git a/python/testData/quickdoc/DirectClass.html b/python/testData/quickdoc/DirectClass.html
index 28d6d74611d3..8991500da1d8 100644
--- a/python/testData/quickdoc/DirectClass.html
+++ b/python/testData/quickdoc/DirectClass.html
@@ -1,2 +1 @@
-class Foo(object)
Doc of Foo.
-
+class Foo(object)
Doc of Foo.
diff --git a/python/testData/quickdoc/DirectFunc.html b/python/testData/quickdoc/DirectFunc.html
index 5f16209f2442..77d22808f803 100644
--- a/python/testData/quickdoc/DirectFunc.html
+++ b/python/testData/quickdoc/DirectFunc.html
@@ -1,2 +1 @@
-def foo()
Doc of foo.
-
+def foo()
Doc of foo.
diff --git a/python/testData/quickdoc/InheritedMethod.html b/python/testData/quickdoc/InheritedMethod.html
index 7d5f0ff2471b..c65a837ca327 100644
--- a/python/testData/quickdoc/InheritedMethod.html
+++ b/python/testData/quickdoc/InheritedMethod.html
@@ -1 +1 @@
-class B
def foo(self)
Documentation is missing. The following if copied from A.foo.
Doc from A.foo.
+class B
def foo(self)
Documentation is missing. The following if copied from A.foo.
Doc from A.foo.
diff --git a/python/testData/quickdoc/Method.html b/python/testData/quickdoc/Method.html
index e0ca3ec964b7..5135547035d0 100644
--- a/python/testData/quickdoc/Method.html
+++ b/python/testData/quickdoc/Method.html
@@ -1 +1 @@
-class Foo
@deco
def meth(self)
Doc of meth.
+class Foo
@deco
def meth(self)
Doc of meth.
diff --git a/python/testData/quickdoc/Module.html b/python/testData/quickdoc/Module.html
index 613f8b40391f..7d51383121a5 100644
--- a/python/testData/quickdoc/Module.html
+++ b/python/testData/quickdoc/Module.html
@@ -1 +1 @@
-Module's doc.
+blah!
Module's doc.