diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index 14a43ebbff35..a2c645c32c76 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -278,6 +278,8 @@ + diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java new file mode 100644 index 000000000000..b9fc3e0375c3 --- /dev/null +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java @@ -0,0 +1,92 @@ +package com.jetbrains.python.configuration; + +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.NonDefaultProjectConfigurable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Comparing; +import com.intellij.ui.CollectionComboBoxModel; +import com.jetbrains.python.documentation.DocStringFormat; +import com.jetbrains.python.documentation.PyDocumentationSettings; +import com.jetbrains.python.testing.PythonTestConfigurationsModel; +import com.jetbrains.python.testing.TestRunnerService; +import org.jetbrains.annotations.Nls; + +import javax.swing.*; +import java.util.List; + +/** + * User: catherine + */ +public class PyIntegratedToolsConfigurable implements Configurable, NonDefaultProjectConfigurable { + private JPanel myMainPanel; + private JComboBox myTestRunnerComboBox; + private JComboBox myDocstringFormatComboBox; + private PythonTestConfigurationsModel myModel; + private Project myProject; + private final PyDocumentationSettings myDocumentationSettings; + + public PyIntegratedToolsConfigurable(Project project) { + myProject = project; + myDocumentationSettings = PyDocumentationSettings.getInstance(project); + myDocstringFormatComboBox.setModel(new CollectionComboBoxModel(DocStringFormat.ALL, myDocumentationSettings.myDocStringFormat)); + } + + @Nls + @Override + public String getDisplayName() { + return "Python Integrated Tools"; + } + + @Override + public Icon getIcon() { + return null; + } + + @Override + public String getHelpTopic() { + return "test_runner_configuration"; + } + + @Override + public JComponent createComponent() { + List configurations = TestRunnerService.getInstance(myProject).getConfigurations(); + myModel = new PythonTestConfigurationsModel(configurations, TestRunnerService.getInstance(myProject).getProjectConfiguration(), + myProject); + updateConfigurations(); + return myMainPanel; + } + + private void updateConfigurations() { + myTestRunnerComboBox.setModel(myModel); + } + + @Override + public boolean isModified() { + if (myTestRunnerComboBox.getSelectedItem() != myModel.getProjectConfiguration()) { + return true; + } + if (!Comparing.equal(myDocstringFormatComboBox.getSelectedItem(), myDocumentationSettings.myDocStringFormat)) { + return true; + } + return false; + } + + @Override + public void apply() throws ConfigurationException { + myModel.apply(); + myDocumentationSettings.myDocStringFormat = (String) myDocstringFormatComboBox.getSelectedItem(); + } + + @Override + public void reset() { + myTestRunnerComboBox.setSelectedItem(myModel.getProjectConfiguration()); + myTestRunnerComboBox.repaint(); + myModel.reset(); + myDocstringFormatComboBox.setSelectedItem(myDocumentationSettings.myDocStringFormat); + } + + @Override + public void disposeUIResources() { + } +} diff --git a/python/src/com/jetbrains/python/documentation/DocStringFormat.java b/python/src/com/jetbrains/python/documentation/DocStringFormat.java new file mode 100644 index 000000000000..63cd229b195f --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/DocStringFormat.java @@ -0,0 +1,19 @@ +package com.jetbrains.python.documentation; + +import com.google.common.collect.ImmutableList; + +import java.util.List; + +/** + * @author yole + */ +public class DocStringFormat { + public static final String PLAIN = "Plain"; + public static final String EPYDOC = "Epydoc"; + public static final String SPHINX = "Sphinx"; + + public static final List ALL = ImmutableList.of(PLAIN, EPYDOC, SPHINX); + + private DocStringFormat() { + } +} diff --git a/python/src/com/jetbrains/python/documentation/EpydocString.java b/python/src/com/jetbrains/python/documentation/EpydocString.java index df76825894ca..883f209364b1 100644 --- a/python/src/com/jetbrains/python/documentation/EpydocString.java +++ b/python/src/com/jetbrains/python/documentation/EpydocString.java @@ -1,5 +1,7 @@ package com.jetbrains.python.documentation; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.xml.util.XmlTagUtilBase; import org.jetbrains.annotations.Nullable; /** @@ -10,6 +12,13 @@ public class EpydocString extends StructuredDocString { super(docstringText, "@"); } + @Override + public String getDescription() { + final String html = inlineMarkupToHTML(myDescription); + assert html != null; + return html; + } + @Override @Nullable public String getReturnType() { @@ -34,34 +43,150 @@ public class EpydocString extends StructuredDocString { if (value == null) { value = getTagValue("param", "**" + paramName); } - return value; + return inlineMarkupToHTML(value); } @Nullable public static String removeInlineMarkup(String s) { + return convertInlineMarkup(s, false); + } + + @Nullable + private static String convertInlineMarkup(String s, boolean toHTML) { if (s == null) return null; - StringBuilder resultBuilder = new StringBuilder(); - int pos = 0; - while(true) { - int bracePos = s.indexOf('{', pos); - if (bracePos < 1) break; - char prevChar = s.charAt(bracePos-1); - if (prevChar >= 'A' && prevChar <= 'Z') { - resultBuilder.append(s.substring(pos, bracePos-1)); - int rbracePos = s.indexOf('}', bracePos); - if (rbracePos < 0) { + MarkupConverter converter = toHTML ? new HTMLConverter() : new MarkupConverter(); + converter.appendWithMarkup(s); + return converter.result(); + } + + private static class MarkupConverter { + protected final StringBuilder myResult = new StringBuilder(); + + public void appendWithMarkup(String s) { + int pos = 0; + while(true) { + int bracePos = s.indexOf('{', pos); + if (bracePos < 1) break; + char prevChar = s.charAt(bracePos-1); + if (prevChar >= 'A' && prevChar <= 'Z') { + appendText(s.substring(pos, bracePos - 1)); + int rbracePos = findMatchingEndBrace(s, bracePos); + if (rbracePos < 0) { + pos = bracePos + 1; + break; + } + final String inlineMarkupContent = s.substring(bracePos + 1, rbracePos); + appendMarkup(prevChar, inlineMarkupContent); + pos = rbracePos + 1; + } + else { + appendText(s.substring(pos, bracePos + 1)); pos = bracePos+1; - break; } - resultBuilder.append(s.substring(bracePos+1, rbracePos)); - pos = rbracePos+1; } - else { - resultBuilder.append(s.substring(pos, bracePos+1)); - pos = bracePos+1; + appendText(s.substring(pos)); + } + + protected void appendText(String text) { + myResult.append(text); + } + + protected void appendMarkup(char markupChar, String markupContent) { + myResult.append(markupContent); + } + + public String result() { + return myResult.toString(); + } + } + + private static class HTMLConverter extends MarkupConverter { + @Override + protected void appendText(String text) { + myResult.append(joinLines(XmlTagUtilBase.escapeString(text, false), true)); + } + + @Override + protected void appendMarkup(char markupChar, String markupContent) { + if (markupChar == 'U') { + appendLink(markupContent); + return; + } + switch (markupChar) { + case 'I': + appendTagPair(markupContent, "i"); + break; + case 'B': + appendTagPair(markupContent, "b"); + break; + case 'C': + appendTagPair(markupContent, "code"); + break; + default: + myResult.append(StringUtil.escapeXml(markupContent)); + break; } } - resultBuilder.append(s.substring(pos)); - return resultBuilder.toString(); + + private void appendTagPair(String markupContent, final String tagName) { + myResult.append("<").append(tagName).append(">"); + appendWithMarkup(markupContent); + myResult.append(""); + } + + private void appendLink(String markupContent) { + String linkText = StringUtil.escapeXml(markupContent); + String linkUrl = linkText; + int pos = markupContent.indexOf('<'); + if (pos >= 0 && markupContent.endsWith(">")) { + linkText = StringUtil.escapeXml(markupContent.substring(0, pos).trim()); + linkUrl = joinLines(StringUtil.escapeXml(markupContent.substring(pos + 1, markupContent.length() - 1)), false); + } + myResult.append("").append(linkText).append(""); + } + + } + + private static int findMatchingEndBrace(String s, int bracePos) { + int braceCount = 1; + for(int pos=bracePos+1; pos < s.length(); pos++) { + char c = s.charAt(pos); + if (c == '{') braceCount++; + else if (c == '}') { + braceCount--; + if (braceCount == 0) return pos; + } + } + return -1; + } + + private static String joinLines(String s, boolean addSpace) { + while(true) { + int lineBreakStart = s.indexOf('\n'); + if (lineBreakStart < 0) break; + int lineBreakEnd = lineBreakStart+1; + int blankLines = 0; + while(lineBreakEnd < s.length() && (s.charAt(lineBreakEnd) == ' ' || s.charAt(lineBreakEnd) == '\n')) { + if (s.charAt(lineBreakEnd) == '\n') blankLines++; + lineBreakEnd++; + } + if (addSpace) { + String separator = blankLines > 0 ? "

" : " "; + s = s.substring(0, lineBreakStart) + separator + s.substring(lineBreakEnd); + } + else { + s = s.substring(0, lineBreakStart) + s.substring(lineBreakEnd); + } + } + return s; + } + + @Nullable + public static String inlineMarkupToHTML(String s) { + return convertInlineMarkup(s, true); } } diff --git a/python/src/com/jetbrains/python/documentation/PyDocumentationSettings.java b/python/src/com/jetbrains/python/documentation/PyDocumentationSettings.java new file mode 100644 index 000000000000..7da47bb9f95a --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/PyDocumentationSettings.java @@ -0,0 +1,36 @@ +package com.jetbrains.python.documentation; + +import com.intellij.openapi.components.*; +import com.intellij.openapi.project.Project; +import com.intellij.util.xmlb.XmlSerializerUtil; + +/** + * @author yole + */ +@State(name = "PyDocumentationSettings", + storages = { + @Storage(id = "default", file = "$PROJECT_FILE$"), + @Storage(id = "dir", file = "$PROJECT_CONFIG_DIR$/other.xml", scheme = StorageScheme.DIRECTORY_BASED) + } +) +public class PyDocumentationSettings implements PersistentStateComponent { + public String myDocStringFormat = DocStringFormat.PLAIN; + + public boolean isEpydocFormat() { + return DocStringFormat.EPYDOC.equals(myDocStringFormat); + } + + public static PyDocumentationSettings getInstance(Project project) { + return ServiceManager.getService(project, PyDocumentationSettings.class); + } + + @Override + public PyDocumentationSettings getState() { + return this; + } + + @Override + public void loadState(PyDocumentationSettings state) { + XmlSerializerUtil.copyBean(state, this); + } +} diff --git a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java index 2add356575c6..9d44c6349c0d 100644 --- a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java +++ b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java @@ -45,10 +45,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -181,8 +178,14 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider impl return cat; } - private static @NotNull ChainIterable combUpDocString(Project project, @NotNull String docstring) { - ChainIterable cat = new ChainIterable(); + private static @NotNull List combUpDocString(Project project, @NotNull String docstring) { + PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(project); + List result = new ArrayList(); + if (documentationSettings.isEpydocFormat()) { + final EpydocString epydocString = new EpydocString(docstring); + result.add(formatStructuredDocString(epydocString)); + return result; + } // detect common indentation String[] lines = LineTokenizer.tokenize(docstring, false); Pattern spaces_pat = Pattern.compile("^\\s+"); @@ -217,7 +220,7 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider impl for (String line : lines) { if (is_first && spaces_pat.matcher(line).matches()) continue; // ignore all initial whitespace if (is_first) is_first = false; - else cat.add(BR); + else result.add(BR); int leadingTabs = 0; while (leadingTabs < line.length() && line.charAt(leadingTabs) == '\t') { leadingTabs++; @@ -225,9 +228,22 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider impl if (leadingTabs > 0) { line = StringUtil.repeatSymbol(' ', tabSize * leadingTabs) + line.substring(leadingTabs); } - cat.add(combUp(line)); + result.add(combUp(line)); } - return cat; + return result; + } + + private static String formatStructuredDocString(StructuredDocString docString) { + StringBuilder result = new StringBuilder(docString.getDescription()); + final List parameters = docString.getParameters(); + if (parameters.size() > 0) { + result.append("
Parameters:
"); + for (String parameter : parameters) { + result.append("").append(parameter).append(": ").append(docString.getParamDescription(parameter)).append("
"); + } + } + return result.toString(); + } // provides ctrl+Q doc diff --git a/python/src/com/jetbrains/python/documentation/StructuredDocString.java b/python/src/com/jetbrains/python/documentation/StructuredDocString.java index 002f8f23da22..36ca2f3c7bbd 100644 --- a/python/src/com/jetbrains/python/documentation/StructuredDocString.java +++ b/python/src/com/jetbrains/python/documentation/StructuredDocString.java @@ -1,16 +1,20 @@ package com.jetbrains.python.documentation; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.Nullable; +import java.util.List; import java.util.Map; /** * @author yole */ public abstract class StructuredDocString { + protected final String myDescription; + protected final List myParameters = Lists.newArrayList(); protected final Map mySimpleTagValues = Maps.newHashMap(); protected final Map> myArgTagValues = Maps.newHashMap(); @@ -24,13 +28,22 @@ public abstract class StructuredDocString { protected StructuredDocString(String docstringText, String tagPrefix) { final String[] lines = LineTokenizer.tokenize(docstringText, false); int i = 0; + StringBuilder descBuilder = new StringBuilder(); while (i < lines.length) { String line = lines[i].trim(); if (line.startsWith(tagPrefix)) { i = parseTag(lines, i, tagPrefix); } + else { + descBuilder.append(line).append("\n"); + } i++; } + myDescription = descBuilder.toString(); + } + + public String getDescription() { + return myDescription; } private int parseTag(String[] lines, int index, String tagPrefix) { @@ -57,6 +70,9 @@ public abstract class StructuredDocString { myArgTagValues.put(tagName, argValues); } argValues.put(arg, value); + if (tagName.equals("param")) { + myParameters.add(arg); + } } return index; } @@ -72,6 +88,10 @@ public abstract class StructuredDocString { return argValues == null ? null : argValues.get(argName); } + public List getParameters() { + return myParameters; + } + @Nullable public abstract String getReturnType(); diff --git a/python/src/com/jetbrains/python/testing/TestRunnerConfigurable.form b/python/src/com/jetbrains/python/testing/PyIntegratedToolsConfigurable.form similarity index 56% rename from python/src/com/jetbrains/python/testing/TestRunnerConfigurable.form rename to python/src/com/jetbrains/python/testing/PyIntegratedToolsConfigurable.form index af8eb74a9225..73365d9acdfd 100644 --- a/python/src/com/jetbrains/python/testing/TestRunnerConfigurable.form +++ b/python/src/com/jetbrains/python/testing/PyIntegratedToolsConfigurable.form @@ -1,6 +1,6 @@ -

- + + @@ -8,7 +8,7 @@ - + @@ -19,14 +19,31 @@ - + - + + + + + + + + + + + + + + + + + + diff --git a/python/src/com/jetbrains/python/testing/TestRunnerConfigurable.java b/python/src/com/jetbrains/python/testing/TestRunnerConfigurable.java deleted file mode 100644 index 4887637350e4..000000000000 --- a/python/src/com/jetbrains/python/testing/TestRunnerConfigurable.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.jetbrains.python.testing; - -import com.intellij.openapi.options.Configurable; -import com.intellij.openapi.options.ConfigurationException; -import com.intellij.openapi.options.NonDefaultProjectConfigurable; -import com.intellij.openapi.project.Project; -import org.jetbrains.annotations.Nls; - -import javax.swing.*; -import java.util.List; - -/** - * User: catherine - */ -public class TestRunnerConfigurable implements Configurable, NonDefaultProjectConfigurable { - private JPanel myMainPanel; - private JComboBox myComboBox; - private PythonTestConfigurationsModel myModel; - private Project myProject; - - public TestRunnerConfigurable(Project project) { - myProject = project; - } - @Nls - @Override - public String getDisplayName() { - return "Test Runner"; - } - - @Override - public Icon getIcon() { - return null; - } - - @Override - public String getHelpTopic() { - return "test_runner_configuration"; - } - - @Override - public JComponent createComponent() { - List configurations = TestRunnerService.getInstance(myProject).getConfigurations(); - myModel = new PythonTestConfigurationsModel(configurations, TestRunnerService.getInstance(myProject).getProjectConfiguration(), - myProject); - updateConfigurations(); - return myMainPanel; - } - - private void updateConfigurations() { - myComboBox.setModel(myModel); - - } - - @Override - public boolean isModified() { - if (myComboBox.getSelectedItem() != myModel.getProjectConfiguration()) { - return true; - } - return false; - } - - @Override - public void apply() throws ConfigurationException { - myModel.apply(); - } - - @Override - public void reset() { - myComboBox.setSelectedItem(myModel.getProjectConfiguration()); - myComboBox.repaint(); - myModel.reset(); - } - - @Override - public void disposeUIResources() { - //To change body of implemented methods use File | Settings | File Templates. - } -} diff --git a/python/testSrc/com/jetbrains/python/EpydocStringTest.java b/python/testSrc/com/jetbrains/python/EpydocStringTest.java index 24c5ca63b20a..4d121b60af14 100644 --- a/python/testSrc/com/jetbrains/python/EpydocStringTest.java +++ b/python/testSrc/com/jetbrains/python/EpydocStringTest.java @@ -1,12 +1,14 @@ package com.jetbrains.python; +import com.intellij.testFramework.UsefulTestCase; import com.jetbrains.python.documentation.EpydocString; -import junit.framework.TestCase; + +import java.util.List; /** * @author yole */ -public class EpydocStringTest extends TestCase { +public class EpydocStringTest extends UsefulTestCase { public void testTagValue() { EpydocString docString = new EpydocString("@rtype: C{str}"); assertEquals("C{str}", docString.getTagValue("rtype")); @@ -50,8 +52,38 @@ public class EpydocStringTest extends TestCase { " @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"); assertEquals("effective UID used to call the function.", docString.getParamDescription("euid")); assertEquals("effective GID used to call the function.", docString.getParamDescription("egid")); assertEquals("arguments passed to function", docString.getParamDescription("args")); } + + public void testInlineMarkupToHTML() { + assertEquals("can contain inline markup and bold text", EpydocString.inlineMarkupToHTML("can contain I{inline markup} and B{bold text}")); + } + + public void testCodeToHTML() { + assertEquals("my_dict={1:2, 3:4}", EpydocString.inlineMarkupToHTML("C{my_dict={1:2, 3:4}}")); + } + + public void testUrlToHTML() { + assertEquals("www.python.org", EpydocString.inlineMarkupToHTML("U{www.python.org}")); + assertEquals("www.python.org", EpydocString.inlineMarkupToHTML("U{www.python.org}")); + assertEquals("The epydoc homepage", + EpydocString.inlineMarkupToHTML("U{The epydoc homepage}")); + } + + public void testNestedInlineMarkup() { + assertEquals("Inline markup may be nested; and it may span multiple lines.", + EpydocString.inlineMarkupToHTML("I{B{Inline markup} may be nested; and\n" + + " it may span} multiple lines.")); + + } + + public void testParagraph() { + assertEquals("foo

bar", EpydocString.inlineMarkupToHTML("foo\n\nbar")); + } }