IDEA-141008 QuickDocumentation (Ctrl-Q) shows "JavaScript is disabled on your browser" sometimes

This commit is contained in:
Dmitry Batrak
2015-06-05 16:49:49 +03:00
parent ac779201db
commit dd78195e3c
7 changed files with 169 additions and 72 deletions
@@ -24,7 +24,6 @@ import com.intellij.lang.java.JavaDocumentationProvider;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NullableComputable;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.VirtualFile;
@@ -54,10 +53,10 @@ import java.util.regex.Pattern;
public class JavaDocExternalFilter extends AbstractExternalFilter {
private final Project myProject;
private static final Trinity<Pattern, Pattern, Boolean> ourPackageInfoSettings = Trinity.create(
private static final ParseSettings ourPackageInfoSettings = new ParseSettings(
Pattern.compile("package\\s+[^\\s]+\\s+description", Pattern.CASE_INSENSITIVE),
Pattern.compile("START OF BOTTOM NAVBAR", Pattern.CASE_INSENSITIVE),
Boolean.TRUE
true, false
);
protected static @NonNls final Pattern ourHTMLsuffix = Pattern.compile("[.][hH][tT][mM][lL]?");
@@ -168,7 +167,7 @@ public class JavaDocExternalFilter extends AbstractExternalFilter {
@NotNull
@Override
protected Trinity<Pattern, Pattern, Boolean> getParseSettings(@NotNull String url) {
protected ParseSettings getParseSettings(@NotNull String url) {
return url.endsWith(JavaDocumentationProvider.PACKAGE_SUMMARY_FILE) ? ourPackageInfoSettings : super.getParseSettings(url);
}
}
@@ -0,0 +1,29 @@
<HTML><base href="placeholder"><style type="text/css"> ul.inheritance {
margin:0;
padding:0;
}
ul.inheritance li {
display:inline;
list-style:none;
}
ul.inheritance li ul.inheritance {
margin-left:15px;
padding-left:15px;
padding-top:1px;
}
</style><a name="param()">
<!-- -->
</a>
<ul class="blockListLast">
<h3><a href="psi_element://com.jetbrains.TestAnnotation"><code>com.jetbrains.TestAnnotation</code></a></h3>
<pre>public abstract&nbsp;java.lang.String&nbsp;param</pre>
<div class="block">Some info</div>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
</HTML>
@@ -30,30 +30,34 @@ import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.testFramework.EditorTestUtil;
import com.intellij.testFramework.PlatformTestCase;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JavaExternalDocumentationTest extends PlatformTestCase {
public void testImagesInsideJavadocJar() throws Exception {
public static final Pattern BASE_URL_PATTERN = Pattern.compile("(<base href=\")([^\"]*)");
public static final Pattern IMG_URL_PATTERN = Pattern.compile("<img src=\"([^\"]*)");
@Override
protected void setUp() throws Exception {
super.setUp();
final VirtualFile libClasses = getJarFile("library.jar");
final VirtualFile libJavadocJar = getJarFile("library-javadoc.jar");
@@ -68,32 +72,33 @@ public class JavaExternalDocumentationTest extends PlatformTestCase {
assertSize(1, modules);
ModuleRootModificationUtil.addDependency(modules[0], library);
});
}
PsiFile psiFile =
PsiFileFactory.getInstance(myProject).createFileFromText(JavaLanguage.INSTANCE, "class Foo { com.jetbrains.Test field; }");
Document document = PsiDocumentManager.getInstance(myProject).getDocument(psiFile);
assertNotNull(document);
Editor editor = EditorFactory.getInstance().createEditor(document, myProject);
try {
editor.getCaretModel().moveToOffset(document.getText().indexOf("Test"));
DocumentationManager documentationManager = DocumentationManager.getInstance(myProject);
documentationManager.showJavaDocInfo(editor, psiFile, false);
waitTillDone(documentationManager.getLastAction());
JBPopup popup = documentationManager.getDocInfoHint();
assertNotNull(popup);
DocumentationComponent documentationComponent = (DocumentationComponent)popup.getContent().getComponent(0);
try {
byte[] imageData = getImageDataFromDocumentationComponent(documentationComponent);
assertEquals(228, imageData.length);
}
finally {
Disposer.dispose(documentationComponent);
}
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
public void testImagesInsideJavadocJar() throws Exception {
String text = getDocumentationText("class Foo { com.jetbrains.<caret>Test field; }");
Matcher baseUrlmatcher = BASE_URL_PATTERN.matcher(text);
assertTrue(baseUrlmatcher.find());
String baseUrl = baseUrlmatcher.group(2);
Matcher imgMatcher = IMG_URL_PATTERN.matcher(text);
assertTrue(imgMatcher.find());
String relativeUrl = imgMatcher.group(1);
URL imageUrl = new URL(new URL(baseUrl), relativeUrl);
try (InputStream stream = imageUrl.openStream()) {
assertEquals(228, FileUtil.loadBytes(stream).length);
}
}
// We're guessing style of references in javadoc by bytecode version of library class file
// but displaying quick doc should work even if javadoc was generated using a JDK not corresponding to bytecode version
public void testReferenceStyleDoesntMatchBytecodeVersion() throws Exception {
String actualText = getDocumentationText("@com.jetbrains.TestAnnotation(<caret>param = \"foo\") class Foo {}");
String expectedText = FileUtil.loadFile(getDataFile(getTestName(false) + ".html"));
assertEquals(expectedText, replaceBaseUrlWithPlaceholder(actualText));
}
private static String replaceBaseUrlWithPlaceholder(String actualText) {
return BASE_URL_PATTERN.matcher(actualText).replaceAll("$1placeholder");
}
private static void waitTillDone(ActionCallback actionCallback) throws InterruptedException {
long start = System.currentTimeMillis();
@@ -106,40 +111,74 @@ public class JavaExternalDocumentationTest extends PlatformTestCase {
fail("Timed out waiting for documentation to show");
}
private static File getDataFile(String name) {
return new File(JavaTestUtil.getJavaTestDataPath() + "/codeInsight/documentation/" + name);
}
@NotNull
private static VirtualFile getJarFile(String name) {
VirtualFile file = getVirtualFile(new File(JavaTestUtil.getJavaTestDataPath() + "/codeInsight/documentation/" + name));
VirtualFile file = getVirtualFile(getDataFile(name));
assertNotNull(file);
VirtualFile jarFile = JarFileSystem.getInstance().getJarRootForLocalFile(file);
assertNotNull(jarFile);
return jarFile;
}
private static byte[] getImageDataFromDocumentationComponent(DocumentationComponent documentationComponent) throws Exception {
JEditorPane editorPane = (JEditorPane)documentationComponent.getComponent();
final HTMLDocument document = (HTMLDocument)editorPane.getDocument();
final Ref<byte[]> result = new Ref<>();
document.render(() -> {
private String getDocumentationText(String sourceEditorText) throws Exception {
int caretPosition = sourceEditorText.indexOf(EditorTestUtil.CARET_TAG);
if (caretPosition >= 0) {
sourceEditorText = sourceEditorText.substring(0, caretPosition) +
sourceEditorText.substring(caretPosition + EditorTestUtil.CARET_TAG.length());
}
PsiFile psiFile = PsiFileFactory.getInstance(myProject).createFileFromText(JavaLanguage.INSTANCE, sourceEditorText);
Document document = PsiDocumentManager.getInstance(myProject).getDocument(psiFile);
assertNotNull(document);
Editor editor = EditorFactory.getInstance().createEditor(document, myProject);
try {
if (caretPosition >= 0) {
editor.getCaretModel().moveToOffset(caretPosition);
}
DocumentationManager documentationManager = DocumentationManager.getInstance(myProject);
MockDocumentationComponent documentationComponent = new MockDocumentationComponent(documentationManager);
try {
HTMLDocument.Iterator it = document.getIterator(HTML.Tag.IMG);
assertTrue(it.isValid());
String relativeUrl = (String)it.getAttributes().getAttribute(HTML.Attribute.SRC);
it.next();
assertFalse(it.isValid());
URL imageUrl = new URL(document.getBase(), relativeUrl);
try (InputStream stream = imageUrl.openStream()) {
result.set(FileUtil.loadBytes(stream));
}
documentationManager.setDocumentationComponent(documentationComponent);
documentationManager.showJavaDocInfo(editor, psiFile, false);
waitTillDone(documentationManager.getLastAction());
return documentationComponent.getText();
}
catch (IOException e) {
throw new RuntimeException(e);
finally {
Disposer.dispose(documentationComponent);
}
});
return result.get();
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
}
}
@Override
protected boolean isRunInWriteAction() {
return false;
}
private static class MockDocumentationComponent extends DocumentationComponent {
private String myText;
public MockDocumentationComponent(DocumentationManager manager) {
super(manager);
}
@Override
public void setText(String text, PsiElement element, boolean clean, boolean clearHistory) {
myText = text;
}
@Override
public void setData(PsiElement _element, String text, boolean clearHistory, String effectiveExternalUrl, String ref) {
myText = text;
}
public String getText() {
return myText;
}
}
}
@@ -21,7 +21,6 @@ import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.openapi.vfs.CharsetToolkit;
@@ -179,10 +178,10 @@ public abstract class AbstractExternalFilter {
}
protected void doBuildFromStream(final String url, Reader input, final StringBuilder data, boolean searchForEncoding, boolean matchStart) throws IOException {
Trinity<Pattern, Pattern, Boolean> settings = getParseSettings(url);
@NonNls Pattern startSection = settings.first;
@NonNls Pattern endSection = settings.second;
boolean useDt = settings.third;
ParseSettings settings = getParseSettings(url);
@NonNls Pattern startSection = settings.startPattern;
@NonNls Pattern endSection = settings.endPattern;
boolean useDt = settings.useDt;
@NonNls String greatestEndSection = "<!-- ========= END OF CLASS DATA ========= -->";
data.append(HTML);
@@ -236,7 +235,7 @@ public abstract class AbstractExternalFilter {
if (read == null) {
data.setLength(0);
if (matchStart && input instanceof MyReader) {
if (matchStart && !settings.forcePatternSearch && input instanceof MyReader) {
try {
final MyReader reader = contentEncoding != null ? new MyReader(((MyReader)input).myInputStream, contentEncoding)
: new MyReader(((MyReader)input).myInputStream, ((MyReader)input).getEncoding());
@@ -299,26 +298,19 @@ public abstract class AbstractExternalFilter {
data.append(HTML_CLOSE);
}
/**
* Decides what settings should be used for parsing content represented by the given url.
*
* @param url url which points to the target content
* @return following data: (start interested data boundary pattern; end interested data boundary pattern;
* replace table data by &lt;dt&gt;)
*/
@NotNull
protected Trinity<Pattern, Pattern, Boolean> getParseSettings(@NotNull String url) {
protected ParseSettings getParseSettings(@NotNull String url) {
Pattern startSection = ourClassDataStartPattern;
Pattern endSection = ourClassDataEndPattern;
boolean useDt = true;
boolean anchorPresent = false;
Matcher anchorMatcher = ourAnchorSuffix.matcher(url);
if (anchorMatcher.find()) {
useDt = false;
anchorPresent = true;
startSection = Pattern.compile(Pattern.quote("<a name=\"" + anchorMatcher.group(1) + "\""), Pattern.CASE_INSENSITIVE);
endSection = ourNonClassDataEndPattern;
}
return Trinity.create(startSection, endSection, useDt);
return new ParseSettings(startSection, endSection, !anchorPresent, anchorPresent);
}
private static boolean reachTheEnd(StringBuilder data, String read, StringBuilder classDetails) {
@@ -441,4 +433,35 @@ public abstract class AbstractExternalFilter {
myInputStream = in;
}
}
/**
* Settings used for parsing of external documentation
*/
protected static class ParseSettings {
@NotNull
/**
* Pattern defining the start of target fragment
*/
private final Pattern startPattern;
@NotNull
/**
* Pattern defining the end of target fragment
*/
private final Pattern endPattern;
/**
* If <code>false</code>, and line matching start pattern is not found, whole document will be processed
*/
private final boolean forcePatternSearch;
/**
* Replace table data by &lt;dt&gt;
*/
private final boolean useDt;
public ParseSettings(@NotNull Pattern startPattern, @NotNull Pattern endPattern, boolean useDt, boolean forcePatternSearch) {
this.startPattern = startPattern;
this.endPattern = endPattern;
this.useDt = useDt;
this.forcePatternSearch = forcePatternSearch;
}
}
}
@@ -103,6 +103,7 @@ public class DocumentationManager extends DockablePopupManager<DocumentationComp
private boolean myCloseOnSneeze;
private ActionCallback myLastAction;
private DocumentationComponent myTestDocumentationComponent;
@Override
protected String getToolwindowId() {
@@ -431,7 +432,8 @@ public class DocumentationManager extends DockablePopupManager<DocumentationComp
PopupUpdateProcessor updateProcessor,
final PsiElement originalElement,
@Nullable final Runnable closeCallback) {
final DocumentationComponent component = new DocumentationComponent(this);
final DocumentationComponent component = myTestDocumentationComponent == null ? new DocumentationComponent(this) :
myTestDocumentationComponent;
component.setNavigateCallback(new Consumer<PsiElement>() {
@Override
public void consume(PsiElement psiElement) {
@@ -1063,6 +1065,11 @@ public class DocumentationManager extends DockablePopupManager<DocumentationComp
public ActionCallback getLastAction() {
return myLastAction;
}
@TestOnly
public void setDocumentationComponent(DocumentationComponent documentationComponent) {
myTestDocumentationComponent = documentationComponent;
}
private interface DocumentationCollector {
@Nullable