| /******************************************************************************* |
| * 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; |
| } |
| |
| } |