/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.ui.internal.text.editor; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Uninterruptibles; import com.google.dart.engine.ast.AstNode; import com.google.dart.engine.ast.Expression; import com.google.dart.engine.ast.MethodDeclaration; import com.google.dart.engine.ast.visitor.ElementLocator; import com.google.dart.engine.element.CompilationUnitElement; import com.google.dart.engine.element.Element; import com.google.dart.engine.element.ExecutableElement; import com.google.dart.engine.element.LibraryElement; import com.google.dart.engine.element.ParameterElement; import com.google.dart.engine.element.PropertyAccessorElement; import com.google.dart.engine.services.util.DartDocUtilities; import com.google.dart.engine.type.Type; import com.google.dart.engine.utilities.general.StringUtilities; import com.google.dart.engine.utilities.source.SourceRange; import com.google.dart.server.GetHoverConsumer; import com.google.dart.tools.core.DartCore; import com.google.dart.tools.core.DartCoreDebug; import com.google.dart.tools.ui.internal.actions.NewSelectionConverter; import com.google.dart.tools.ui.internal.problemsview.ProblemsView; import com.google.dart.tools.ui.internal.util.GridDataFactory; import com.google.dart.tools.ui.internal.util.GridLayoutFactory; import com.google.dart.tools.ui.text.DartSourceViewerConfiguration; import org.apache.commons.lang3.text.WordUtils; import org.dartlang.analysis.server.protocol.HoverInformation; import org.dartlang.analysis.server.protocol.RequestError; import org.eclipse.core.resources.IMarker; import org.eclipse.jface.text.AbstractInformationControl; import org.eclipse.jface.text.AbstractReusableInformationControlCreator; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IInformationControl; import org.eclipse.jface.text.IInformationControlCreator; import org.eclipse.jface.text.IInformationControlExtension2; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextHover; import org.eclipse.jface.text.ITextHoverExtension; import org.eclipse.jface.text.ITextHoverExtension2; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.ISourceViewerExtension2; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.forms.widgets.FormToolkit; import org.eclipse.ui.forms.widgets.Section; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.ui.texteditor.MarkerAnnotation; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class DartHover implements ITextHover, ITextHoverExtension, ITextHoverExtension2 { private static class AnnotationsSection { private final FormToolkit toolkit; private final Section section; private final Composite container; public AnnotationsSection(Composite parent, String title) { toolkit = createToolkit(parent.getDisplay()); this.section = toolkit.createSection(parent, Section.TITLE_BAR); GridDataFactory.create(section).grabHorizontal().fill(); section.setText(title); container = toolkit.createComposite(section); GridLayoutFactory.create(container).columns(2).spacingHorizontal(0); section.setClient(container); } public void setAnnotations(List<Annotation> annotations) { for (Control child : container.getChildren()) { child.dispose(); } annotations = getSortedAnnotations(annotations); for (Annotation annotation : annotations) { // prepare marker IMarker marker = null; if (annotation instanceof MarkerAnnotation) { marker = ((MarkerAnnotation) annotation).getMarker(); } // icon { Label imageLabel = new Label(container, SWT.NONE); if (marker != null) { imageLabel.setImage(ProblemsView.DESCRIPTION_LABEL_PROVIDER.getImage(marker)); } } // message toolkit.createLabel(container, annotation.getText()); // correction if (marker != null) { String correction = marker.getAttribute(DartCore.MARKER_ATTR_CORRECTION, (String) null); if (correction != null) { new Label(container, SWT.NONE); toolkit.createLabel(container, correction); } } } } } private static class DartInformationControl extends AbstractInformationControl implements IInformationControlExtension2 { private static final Point SIZE_CONSTRAINTS = new Point(10000, 10000); private static boolean isGridVisible(AnnotationsSection section) { return section.section.getVisible(); } private static boolean isGridVisible(DocSection section) { return section.section.getVisible(); } private static boolean isGridVisible(TextSection section) { return section.section.getVisible(); } private static void setGridVisible(AnnotationsSection section, boolean visible) { setGridVisible(section.section, visible); } private static void setGridVisible(Control control, boolean visible) { GridDataFactory.modify(control).exclude(!visible); control.setVisible(visible); control.getParent().layout(); } private static void setGridVisible(DocSection section, boolean visible) { setGridVisible(section.section, visible); } private static void setGridVisible(TextSection section, boolean visible) { setGridVisible(section.section, visible); } private boolean hasContents; private Composite container; private TextSection elementSection; private TextSection classSection; private TextSection librarySection; private AnnotationsSection problemsSection; private DocSection docSection; private TextSection staticTypeSection; private TextSection propagatedTypeSection; private TextSection parameterSection; public DartInformationControl(Shell parentShell) { super(parentShell, false); toolkit = createToolkit(parentShell.getDisplay()); create(); } @Override public Point computeSizeConstraints(int widthInChars, int heightInChars) { return SIZE_CONSTRAINTS; } @Override public Point computeSizeHint() { // Shell was already packed and has the required size. return getShell().getSize(); } @Override public IInformationControlCreator getInformationPresenterControlCreator() { return new DartInformationControlCreator(); } @Override public boolean hasContents() { return hasContents; } @Override public void setInput(Object input) { hasContents = false; // Hide all sections. setGridVisible(elementSection, false); setGridVisible(classSection, false); setGridVisible(librarySection, false); setGridVisible(problemsSection, false); setGridVisible(docSection, false); setGridVisible(staticTypeSection, false); setGridVisible(propagatedTypeSection, false); setGridVisible(parameterSection, false); if (input instanceof HoverInfo_NEW) { // // Display hover based on Analysis Server response // HoverInformation hover = ((HoverInfo_NEW) input).hover; if (hover != null) { // Element if (hover.getElementKind() != null) { // show Element { String description = hover.getElementDescription(); if (description != null) { String text = WordUtils.wrap(description, 100); setGridVisible(elementSection, true); elementSection.setTitle(WordUtils.capitalize(hover.getElementKind())); elementSection.setText(text); } } // show Class { String className = hover.getContainingClassDescription(); if (className != null) { setGridVisible(classSection, true); classSection.setText(className); } } // show Library { String unitName = hover.getContainingLibraryPath(); String libraryName = hover.getContainingLibraryName(); if (unitName != null && libraryName != null) { String text = StringUtilities.abbreviateLeft(libraryName, 25) + " | " + StringUtilities.abbreviateLeft(unitName, 35); setGridVisible(librarySection, true); librarySection.setText(text); } } // Dart Doc { String dartDoc = hover.getDartdoc(); if (dartDoc != null) { setGridVisible(docSection, true); docSection.setDoc(dartDoc); } } } // parameter { String parameter = hover.getParameter(); if (parameter != null) { setGridVisible(parameterSection, true); parameterSection.setText(parameter); } } // static type { String staticType = hover.getStaticType(); if (staticType != null) { setGridVisible(staticTypeSection, true); staticTypeSection.setText(staticType); } } // propagated type { String propagatedType = hover.getPropagatedType(); if (propagatedType != null) { setGridVisible(propagatedTypeSection, true); propagatedTypeSection.setText(propagatedType); } } } // Annotations. { List<Annotation> annotations = ((HoverInfo_NEW) input).annotations; int size = annotations.size(); if (size != 0) { setGridVisible(problemsSection, true); problemsSection.setAnnotations(annotations); } } } else if (input instanceof HoverInfo_OLD) { // // Display hover based upon java base Analysis Engine information // HoverInfo_OLD hoverInfo = (HoverInfo_OLD) input; AstNode node = hoverInfo.node; Element element = hoverInfo.element; // Element if (element != null) { // show variable, if synthetic accessor if (element instanceof PropertyAccessorElement) { PropertyAccessorElement accessor = (PropertyAccessorElement) element; if (accessor.isSynthetic()) { element = accessor.getVariable(); } } // show Element { String text = element.toString(); text = WordUtils.wrap(text, 100); setGridVisible(elementSection, true); elementSection.setTitle(WordUtils.capitalize(element.getKind().getDisplayName())); elementSection.setText(text); } // show Library { LibraryElement library = element.getLibrary(); CompilationUnitElement unit = element.getAncestor(CompilationUnitElement.class); if (library != null && unit != null) { String unitName = unit.getSource().getFullName(); String libraryName = library.getDisplayName(); String text = StringUtilities.abbreviateLeft(libraryName, 25) + " | " + StringUtilities.abbreviateLeft(unitName, 35); setGridVisible(librarySection, true); librarySection.setText(text); } } // Dart Doc try { String dartDoc = element.computeDocumentationComment(); if (dartDoc != null) { dartDoc = DartDocUtilities.cleanDartDoc(dartDoc); setGridVisible(docSection, true); docSection.setDoc(dartDoc); } } catch (Throwable e) { } } // types if (node instanceof Expression) { Expression expression = (Expression) node; // parameter { AstNode n = expression; while (n != null) { if (n instanceof Expression) { ParameterElement parameterElement = ((Expression) n).getBestParameterElement(); if (parameterElement != null) { setGridVisible(parameterSection, true); parameterSection.setText(DartDocUtilities.getTextSummary(null, parameterElement)); break; } } n = n.getParent(); } } // static type Type staticType = expression.getStaticType(); if (staticType != null && element == null) { setGridVisible(staticTypeSection, true); staticTypeSection.setText(staticType.getDisplayName()); } // propagated type if (!(element instanceof ExecutableElement)) { Type propagatedType = expression.getPropagatedType(); if (propagatedType != null && !propagatedType.equals(staticType)) { setGridVisible(propagatedTypeSection, true); propagatedTypeSection.setText(propagatedType.getDisplayName()); } } } // Annotations. { List<Annotation> annotations = hoverInfo.annotations; int size = annotations.size(); if (size != 0) { setGridVisible(problemsSection, true); problemsSection.setAnnotations(annotations); } } } else { return; } // update 'hasContents' flag hasContents |= isGridVisible(elementSection); hasContents |= isGridVisible(librarySection); hasContents |= isGridVisible(problemsSection); hasContents |= isGridVisible(docSection); hasContents |= isGridVisible(staticTypeSection); hasContents |= isGridVisible(propagatedTypeSection); hasContents |= isGridVisible(parameterSection); // Layout and pack. Shell shell = getShell(); shell.layout(true, true); shell.pack(); shell.layout(true, true); shell.pack(); } @Override protected void createContent(Composite parent) { container = toolkit.createComposite(parent); GridLayoutFactory.create(container); elementSection = new TextSection(container, "Element"); classSection = new TextSection(container, "Containing class"); librarySection = new TextSection(container, "Containing library"); problemsSection = new AnnotationsSection(container, "Problems"); docSection = new DocSection(container, "Documentation"); staticTypeSection = new TextSection(container, "Static type"); propagatedTypeSection = new TextSection(container, "Propagated type"); parameterSection = new TextSection(container, "Parameter"); } } private static class DartInformationControlCreator extends AbstractReusableInformationControlCreator { @Override protected IInformationControl doCreateInformationControl(Shell parent) { return new DartInformationControl(parent); } } private static class DocSection { private final FormToolkit toolkit; private final Section section; private final StyledText textWidget; public DocSection(Composite parent, String title) { toolkit = createToolkit(parent.getDisplay()); this.section = toolkit.createSection(parent, Section.TITLE_BAR); GridDataFactory.create(section).grab().fill(); section.setText(title); // create Composite to draw flat border Composite body = toolkit.createComposite(section); GridLayoutFactory.create(body).margins(2); section.setClient(body); // create StyledText widget textWidget = new StyledText(body, SWT.H_SCROLL | SWT.V_SCROLL); textWidget.setMargins(5, 5, 5, 5); // We do this to prevent line spacing changing. // See https://code.google.com/p/dart/issues/detail?id=15899 textWidget.setLineSpacing(1); // configure flat border textWidget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER); toolkit.paintBordersFor(body); } public void setDoc(String doc) { textWidget.setText(doc); textWidget.setSelection(0); // apply size Point requiredSize = textWidget.computeSize(SWT.DEFAULT, SWT.DEFAULT); GridDataFactory gdf = GridDataFactory.create(textWidget); int maxWidth = gdf.convertWidthInCharsToPixels(85); int maxHeight = gdf.convertHeightInCharsToPixels(15); int width = Math.min(requiredSize.x, maxWidth); int height = Math.min(requiredSize.y, maxHeight); gdf.hint(width, height).grab().fill(); } } private static class HoverInfo_NEW { private HoverInformation hover; private List<Annotation> annotations; public HoverInfo_NEW(HoverInformation hover, List<Annotation> annotations) { this.hover = hover; this.annotations = annotations; } } private static class HoverInfo_OLD { AstNode node; Element element; List<Annotation> annotations; public HoverInfo_OLD(AstNode node, Element element, List<Annotation> annotations) { this.node = node; this.element = element; this.annotations = annotations; } } private static class TextSection { private final FormToolkit toolkit; private final Section section; private final StyledText textWidget; public TextSection(Composite parent, String title) { toolkit = createToolkit(parent.getDisplay()); this.section = toolkit.createSection(parent, Section.TITLE_BAR); GridDataFactory.create(section).grabHorizontal().fill(); section.setText(title); textWidget = new StyledText(section, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP); toolkit.adapt(textWidget, false, false); section.setClient(textWidget); } public void setText(String text) { textWidget.setText(text); textWidget.setSelection(0); } public void setTitle(String title) { section.setText(title); } } private static final List<ITextHover> hoverContributors = Lists.newArrayList(); private static FormToolkit toolkit; /** * Register a {@link ITextHover} tooltip contributor. */ public static void addContributer(ITextHover hoverContributor) { hoverContributors.add(hoverContributor); } private static FormToolkit createToolkit(Display display) { if (toolkit == null) { toolkit = new FormToolkit(display); } return toolkit; } /** * Sorts given {@link Annotation}s by severity and location. */ private static List<Annotation> getSortedAnnotations(List<Annotation> annotations) { annotations = Lists.newArrayList(annotations); Collections.sort(annotations, new Comparator<Annotation>() { @Override public int compare(Annotation o1, Annotation o2) { IMarker m1 = getMarker(o1); IMarker m2 = getMarker(o2); // no marker(s) if (m1 != null && m2 == null) { return 1; } if (m1 == null && m2 != null) { return -1; } if (m1 == null && m2 == null) { return 0; } // compare severity int val = m2.getAttribute(IMarker.SEVERITY, 0) - m1.getAttribute(IMarker.SEVERITY, 0); if (val != 0) { return val; } // compare offset return m2.getAttribute(IMarker.CHAR_START, 0) - m1.getAttribute(IMarker.CHAR_START, 0); } private IMarker getMarker(Annotation annotation) { return (annotation instanceof MarkerAnnotation) ? ((MarkerAnnotation) annotation).getMarker() : null; } }); return annotations; } private final ISourceViewer viewer; private final DartSourceViewerConfiguration viewerConfiguration; private CompilationUnitEditor editor; private IInformationControlCreator informationControlCreator; private ITextHover lastReturnedHover; private int lastClickOffset; public DartHover(ITextEditor editor, ISourceViewer viewer, DartSourceViewerConfiguration viewerConfiguration) { this.viewer = viewer; this.viewerConfiguration = viewerConfiguration; if (editor instanceof CompilationUnitEditor) { this.editor = (CompilationUnitEditor) editor; StyledText textWidget = this.editor.getViewer().getTextWidget(); textWidget.addMouseListener(new MouseAdapter() { @Override public void mouseDown(MouseEvent e) { SourceRange range = DartHover.this.editor.getTextSelectionRange(); lastClickOffset = range != null ? range.getOffset() : -1; } }); textWidget.addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent e) { lastClickOffset = -1; } }); } } @Override public IInformationControlCreator getHoverControlCreator() { if (lastReturnedHover instanceof ITextHoverExtension) { return ((ITextHoverExtension) lastReturnedHover).getHoverControlCreator(); } if (informationControlCreator == null) { informationControlCreator = new DartInformationControlCreator(); } return informationControlCreator; } @Override public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) { lastReturnedHover = null; return null; } @Override public Object getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion) { lastReturnedHover = null; // Check through the contributed hover providers. for (ITextHover hoverContributer : hoverContributors) { if (hoverContributer instanceof ITextHoverExtension2) { Object hoverInfo = ((ITextHoverExtension2) hoverContributer).getHoverInfo2( textViewer, hoverRegion); if (hoverInfo != null) { lastReturnedHover = hoverContributer; return hoverInfo; } } } // Editor based hover. if (editor != null) { List<Annotation> annotations = getAnnotations(hoverRegion); // prepare node int offset = hoverRegion.getOffset(); if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { String file = editor.getInputFilePath(); if (file != null) { final CountDownLatch latch = new CountDownLatch(1); final HoverInformation[] hoverInformation = new HoverInformation[1]; DartCore.getAnalysisServer().analysis_getHover(file, offset, new GetHoverConsumer() { @Override public void computedHovers(HoverInformation[] hovers) { if (hovers != null && hovers.length > 0) { hoverInformation[0] = hovers[0]; latch.countDown(); } } @Override public void onError(RequestError requestError) { latch.countDown(); } }); // This executes on a background thread that does not hold the workspace lock // so block until analysis server responds or time expires. // Wait a long time only if there is nothing else to show long waitTimeMillis = annotations.isEmpty() ? 4000 : 500; Uninterruptibles.awaitUninterruptibly(latch, waitTimeMillis, TimeUnit.MILLISECONDS); return new HoverInfo_NEW(hoverInformation[0], annotations); } } else { AstNode node = NewSelectionConverter.getNodeAtOffset(editor, offset); if (node instanceof MethodDeclaration) { MethodDeclaration method = (MethodDeclaration) node; node = method.getName(); } // show Expression if (node instanceof Expression) { Element element = ElementLocator.locateWithOffset(node, offset); return new HoverInfo_OLD(node, element, annotations); } } // always show annotations, even if no node if (!annotations.isEmpty()) { return new HoverInfo_OLD(null, null, annotations); } } return null; } @Override public IRegion getHoverRegion(ITextViewer textViewer, int offset) { IRegion wordRange = findWord(textViewer.getDocument(), offset); // ignore word if it was clicked { int wordOffset = wordRange.getOffset(); int wordEnd = wordOffset + wordRange.getLength(); if (wordOffset <= lastClickOffset && lastClickOffset <= wordEnd) { return null; } } // OK return wordRange; } private IRegion findWord(IDocument document, int offset) { int start = -2; int end = -1; try { int pos = offset; char c; while (pos >= 0) { c = document.getChar(pos); if (!Character.isUnicodeIdentifierPart(c)) { break; } --pos; } start = pos; pos = offset; int length = document.getLength(); while (pos < length) { c = document.getChar(pos); if (!Character.isUnicodeIdentifierPart(c)) { break; } ++pos; } end = pos; } catch (BadLocationException x) { } if (start >= -1 && end > -1) { if (start == offset && end == offset) { return new Region(offset, 0); } else if (start == offset) { return new Region(start, end - start); } else { return new Region(start + 1, end - start - 1); } } return null; } private IAnnotationModel getAnnotationModel() { if (viewer instanceof ISourceViewerExtension2) { ISourceViewerExtension2 extension = (ISourceViewerExtension2) viewer; return extension.getVisualAnnotationModel(); } return viewer.getAnnotationModel(); } private List<Annotation> getAnnotations(IRegion region) { List<Annotation> annotations = Lists.newArrayList(); IAnnotationModel model = getAnnotationModel(); if (model != null) { @SuppressWarnings("unchecked") Iterator<Annotation> iter = model.getAnnotationIterator(); while (iter.hasNext()) { Annotation annotation = iter.next(); if (viewerConfiguration.isShownInText(annotation)) { Position p = model.getPosition(annotation); if (p != null && p.overlapsWith(region.getOffset(), region.getLength())) { String msg = annotation.getText(); if (msg != null && msg.trim().length() > 0) { annotations.add(annotation); } } } } } return annotations; } }