blob: 890e6c651f03d4319897afe850307347868ab92f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2017 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* xored software, Inc. - initial API and Implementation (Andrei Sobolev)
* xored software, Inc. - RubyDocumentation display improvements (Alex Panchenko <alex@xored.com>)
*******************************************************************************/
package org.eclipse.dltk.ruby.internal.ui.documentation;
import java.io.Reader;
import java.io.StringReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.dltk.ast.Modifiers;
import org.eclipse.dltk.core.IField;
import org.eclipse.dltk.core.IMember;
import org.eclipse.dltk.core.IMethod;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ISourceRange;
import org.eclipse.dltk.core.IType;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.internal.core.BuiltinProjectFragment;
import org.eclipse.dltk.ruby.core.PredefinedVariables;
import org.eclipse.dltk.ruby.core.model.FakeField;
import org.eclipse.dltk.ruby.internal.ui.docs.RiHelper;
import org.eclipse.dltk.ruby.internal.ui.text.IRubyPartitions;
import org.eclipse.dltk.ruby.internal.ui.text.RubyPartitionScanner;
import org.eclipse.dltk.ui.documentation.IScriptDocumentationProvider;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.rules.FastPartitioner;
public class RubyDocumentationProvider implements IScriptDocumentationProvider {
private static final String PUBLIC = "public"; //$NON-NLS-1$
private static final String PROTECTED = "protected"; //$NON-NLS-1$
private static final String PRIVATE = "private"; //$NON-NLS-1$
protected String getLine(Document d, int line) throws BadLocationException {
return d.get(d.getLineOffset(line), d.getLineLength(line));
}
/**
* Installs a partitioner with <code>document</code>.
*
* @param document
* the document
*/
private static void installStuff(Document document) {
String[] types = new String[] { IRubyPartitions.RUBY_STRING,
IRubyPartitions.RUBY_SINGLE_QUOTE_STRING,
IRubyPartitions.RUBY_PERCENT_STRING,
IRubyPartitions.RUBY_COMMENT, IRubyPartitions.RUBY_DOC,
IDocument.DEFAULT_CONTENT_TYPE };
FastPartitioner partitioner = new FastPartitioner(
new RubyPartitionScanner(), types);
partitioner.connect(document);
document.setDocumentPartitioner(IRubyPartitions.RUBY_PARTITIONING,
partitioner);
}
/**
* Removes partitioner with <code>document</code>.
*
* @param document
* the document
*/
private static void removeStuff(Document document) {
document.setDocumentPartitioner(IRubyPartitions.RUBY_PARTITIONING,
null);
}
private static int findOffsetBeforeMethod(Document doc, int start)
throws BadLocationException {
int line = doc.getLineOfOffset(start);
for (;;) {
if (--line < 0) {
throw new BadLocationException();
}
final IRegion r = doc.getLineInformation(line);
if (r.getLength() == 0) {
continue;
}
String s = doc.get(r.getOffset(), r.getLength());
if (isBlank(s)) {
continue;
}
s = s.trim();
if (PUBLIC.equals(s) || PROTECTED.equals(s) || PRIVATE.equals(s)) {
/**
* skip access modifiers between method and comment, e.g.
*
* <code>
* # foo-method documentation
* public
* def foo
* end
* </code>
*/
continue;
}
return r.getOffset() + r.getLength() - 1;
}
}
public static String getHeaderComment(String contents, int offset) {
int start = offset;
int end = start;
Document doc = new Document(contents);
installStuff(doc);
try {
int pos = 0;
if (start > 0) {
pos = findOffsetBeforeMethod(doc, start);
}
while (pos >= 0 && pos <= doc.getLength()) {
ITypedRegion region = TextUtilities.getPartition(doc,
IRubyPartitions.RUBY_PARTITIONING, pos, true);
if (region.getType().equals(IRubyPartitions.RUBY_DOC) || region
.getType().equals(IRubyPartitions.RUBY_COMMENT)) {
start = region.getOffset();
}
if (region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE)) {
String content = doc
.get(region.getOffset(), region.getLength()).trim();
if (content.length() > 0 && !content.startsWith("public") //$NON-NLS-1$
&& !content.startsWith("protected") //$NON-NLS-1$
&& !content.startsWith("private")) //$NON-NLS-1$
break;
}
pos = region.getOffset() - 1;
}
pos = start + 1;
while (pos <= doc.getLength()) {
ITypedRegion region = TextUtilities.getPartition(doc,
IRubyPartitions.RUBY_PARTITIONING, pos, true);
if (region.getType().equals(IRubyPartitions.RUBY_DOC) || region
.getType().equals(IRubyPartitions.RUBY_COMMENT)) {
end = region.getOffset() + region.getLength();
}
if (region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE)) {
String content = doc
.get(region.getOffset(), region.getLength()).trim();
if (content.length() > 0 && !content.startsWith("public") //$NON-NLS-1$
&& !content.startsWith("protected") //$NON-NLS-1$
&& !content.startsWith("private")) //$NON-NLS-1$
break;
}
pos = region.getOffset() + region.getLength() + 1;
}
if (end >= doc.getLength())
end = doc.getLength() - 1;
return doc.get(start, end - start);
} catch (BadLocationException e1) {
return null;
} finally {
removeStuff(doc);
}
}
protected String getHeaderComment(IMember member) {
if (member instanceof IField) {
try {
if ((member.getFlags() & Modifiers.AccConstant) == 0) {
return null;
}
} catch (ModelException e) {
return null;
}
}
try {
ISourceRange range = member.getSourceRange();
if (range == null)
return null;
int offset = range.getOffset();
ISourceModule sourceModule = member.getSourceModule();
if (!sourceModule.isConsistent()) {
return null;
}
String contents = sourceModule.getSource();
return RubyDocumentationProvider.getHeaderComment(contents, offset);
} catch (ModelException e) {
}
return null;
}
private Reader proccessBuiltinType(IType type) {
String keyword = type.getElementName();
RiHelper helper = RiHelper.getInstance();
String doc = helper.getDocFor(keyword);
if (doc != null)
return new StringReader(doc);
return null;
}
private static final String NOTHING_KNOWN_ABOUT = "Nothing known about"; //$NON-NLS-1$
private Reader proccessBuiltinMethod(IMethod method) {
final String divider = "#"; //$NON-NLS-1$
IModelElement pp = method.getAncestor(IModelElement.TYPE);
if (pp == null)
return null;
if (pp.getElementName().startsWith("<<")) //$NON-NLS-1$
pp = pp.getAncestor(IModelElement.TYPE);
String keyword = pp.getElementName() + divider
+ method.getElementName();
RiHelper helper = RiHelper.getInstance();
String doc = helper.getDocFor(keyword);
if (doc != null
&& (doc.indexOf(NOTHING_KNOWN_ABOUT) >= 0 || isBlank(doc))) {
// XXX megafix: some Kernel methods are documented in Object
if (pp.getElementName().equals("Kernel")) { //$NON-NLS-1$
keyword = "Object" + divider + method.getElementName(); //$NON-NLS-1$
doc = helper.getDocFor(keyword);
if (doc != null && doc.indexOf(NOTHING_KNOWN_ABOUT) >= 0) {
doc = null;
}
} else {
doc = null;
}
}
if (doc != null)
return new StringReader(doc);
return null;
}
@Override
public Reader getInfo(IMember member, boolean lookIntoParents,
boolean lookIntoExternal) {
boolean isBuiltin = member.getAncestor(
IModelElement.PROJECT_FRAGMENT) instanceof BuiltinProjectFragment;
if (isBuiltin && member instanceof IMethod) {
IMethod method = (IMethod) member;
return proccessBuiltinMethod(method);
} else if (isBuiltin && member instanceof IType) {
IType type = (IType) member;
return proccessBuiltinType(type);
} else if (member instanceof FakeField) {
FakeField field = (FakeField) member;
String doc = PredefinedVariables.getDocOf(field.getElementName());
if (doc != null)
return new StringReader(doc);
}
String header = getHeaderComment(member);
if (header == null || header.length() == 0)
return null;
return new StringReader(convertToHTML(header));
}
private static String replaceSpecTag(String original, String sc,
String tag) {
String filtered = original;
if (sc.equals("*") || sc.equals("+")) //$NON-NLS-1$ //$NON-NLS-2$
sc = "\\" + sc; //$NON-NLS-1$
Pattern bold = Pattern.compile(sc + "[_a-zA-Z0-9]+" + sc); //$NON-NLS-1$
while (true) {
Matcher matcher = bold.matcher(filtered);
if (matcher.find()) {
String startStr = filtered.substring(0, matcher.start());
String endStr = filtered.substring(matcher.end());
String grp = matcher.group();
filtered = startStr + "<" + tag + ">" //$NON-NLS-1$ //$NON-NLS-2$
+ grp.substring(1, grp.length() - 1) + "</" + tag + ">" //$NON-NLS-1$ //$NON-NLS-2$
+ endStr;
} else
break;
}
return filtered;
}
protected String convertToHTML(String header) {
if (header == null)
return ""; //$NON-NLS-1$
StringBuffer result = new StringBuffer();
Document d = new Document(header);
boolean enabled = true;
for (int line = 0;; line++) {
try {
String str = getLine(d, line).trim();
if (str == null)
break;
if (str.startsWith("#--")) { //$NON-NLS-1$
enabled = false;
} else if (str.startsWith("#++")) { //$NON-NLS-1$
enabled = true;
continue;
}
if (!enabled)
continue;
if (str.startsWith("=begin")) //$NON-NLS-1$
continue;
if (str.startsWith("=end")) //$NON-NLS-1$
continue;
while (str.length() > 0 && str.startsWith("#")) //$NON-NLS-1$
str = str.substring(1);
str = replaceSpecTag(str, "*", "b"); //$NON-NLS-1$ //$NON-NLS-2$
str = replaceSpecTag(str, "+", "tt"); //$NON-NLS-1$ //$NON-NLS-2$
str = replaceSpecTag(str, "_", "em"); //$NON-NLS-1$ //$NON-NLS-2$
str.replaceAll("\\*[_a-zA-Z0-9]+\\*", ""); //$NON-NLS-1$ //$NON-NLS-2$
if (str.length() == 0)
result.append("<p>"); //$NON-NLS-1$
else {
if (str.trim().startsWith("== ")) { //$NON-NLS-1$
result.append("<h2>"); //$NON-NLS-1$
result.append(str.substring(3));
result.append("</h2>"); //$NON-NLS-1$
} else if (str.trim().startsWith("= ")) { //$NON-NLS-1$
result.append("<h1>"); //$NON-NLS-1$
result.append(str.substring(2));
result.append("</h1>"); //$NON-NLS-1$
} else if (str.trim().startsWith("---")) { //$NON-NLS-1$
result.append("<hr>"); //$NON-NLS-1$
} else {
result.append(str + "<br>"); //$NON-NLS-1$
}
}
} catch (BadLocationException e) {
break;
}
}
// result.append("</p>\n");
return result.toString();
}
@Override
public Reader getInfo(String content) {
return null;
}
/**
* Checks if a String is whitespace, empty ("") or null.
*
* @param str
* the String to check, may be null
* @return <code>true</code> if the String is null, empty or whitespace
*/
private static boolean isBlank(String s) {
if (s != null) {
final int len = s.length();
if (len != 0) {
for (int i = 0; i < len; i++) {
if (!Character.isWhitespace(s.charAt(i))) {
return false;
}
}
}
}
return true;
}
}