package com.redhat.ceylon.eclipse.code.hover; import static java.lang.Math.max; import static java.lang.Math.min; import java.util.Arrays; import java.util.Comparator; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IInformationControlExtension2; import org.eclipse.jface.text.IRewriteTarget; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.ITextViewerExtension; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.viewers.StyledString; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Canvas; 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.Link; import org.eclipse.swt.widgets.ProgressBar; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess; import com.redhat.ceylon.compiler.typechecker.tree.Tree; import com.redhat.ceylon.eclipse.code.editor.CeylonEditor; import com.redhat.ceylon.eclipse.code.editor.CeylonInitializerAnnotation; import com.redhat.ceylon.eclipse.code.editor.RefinementAnnotation; import com.redhat.ceylon.eclipse.code.parse.CeylonParseController; import com.redhat.ceylon.eclipse.ui.CeylonPlugin; import com.redhat.ceylon.eclipse.util.Highlights; import com.redhat.ceylon.ide.common.util.escaping_; /** * The annotation information control shows informations about a given * {@link AnnotationInfo}. It can also show a toolbar * and a list of {@link ICompletionProposal}s. * * @since 3.4 */ class AnnotationInformationControl extends AbstractInformationControl implements IInformationControlExtension2 { private final DefaultMarkerAnnotationAccess fMarkerAnnotationAccess; private Control fFocusControl; private AnnotationInfo fInput; private Composite fParent; private final ISchedulingRule retrievingQuickFixesRule = new ISchedulingRule() { @Override public boolean isConflicting(ISchedulingRule rule) { return rule == this; } @Override public boolean contains(ISchedulingRule rule) { return rule == this; } }; AtomicBoolean quickFixesRetrieved = new AtomicBoolean(false); AnnotationInformationControl(Shell parentShell, String statusFieldText) { super(parentShell, statusFieldText); fMarkerAnnotationAccess = new DefaultMarkerAnnotationAccess(); create(); } AnnotationInformationControl(Shell parentShell, ToolBarManager toolBarManager) { super(parentShell, toolBarManager); fMarkerAnnotationAccess = new DefaultMarkerAnnotationAccess(); create(); } @Override public void setInformation(String information) { //replaced by IInformationControlExtension2#setInput } public Tree.CompilationUnit getTypecheckedRootNode(AnnotationInfo info) { if (info == null) { return null; } CeylonEditor editor = info.getEditor(); if (editor == null) { return null; } CeylonParseController cpc = editor.getParseController(); if (cpc == null) { return null; } return cpc.getTypecheckedRootNode(); } @Override public void setInput(Object input) { Assert.isLegal(input instanceof AnnotationInfo); fInput = (AnnotationInfo) input; disposeDeferredCreatedContent(); deferredCreateContent(); } @Override public boolean hasContents() { return fInput != null; } AnnotationInfo getAnnotationInfo() { return fInput; } @Override public void setFocus() { if (!getShell().isDisposed()) { super.setFocus(); if (fFocusControl != null) { fFocusControl.setFocus(); } } } @Override public final void setVisible(boolean visible) { if (!visible) { disposeDeferredCreatedContent(); } super.setVisible(visible); } protected void disposeDeferredCreatedContent() { Control[] children = fParent.getChildren(); for (int i=0; i<children.length; i++) { children[i].dispose(); } quickFixesRetrieved.set(false); ToolBarManager toolBarManager = getToolBarManager(); if (toolBarManager != null) toolBarManager.removeAll(); } @Override protected void createContent(Composite parent) { fParent = parent; quickFixesRetrieved.set(false); GridLayout layout = new GridLayout(1, false); layout.verticalSpacing = 0; layout.marginWidth = 0; layout.marginHeight = 0; fParent.setLayout(layout); } @Override public Point computeSizeHint() { Point preferedSize = getShell() .computeSize(SWT.DEFAULT, SWT.DEFAULT, true); Point constrains = getSizeConstraints(); if (constrains == null) { return preferedSize; } int trimWidth = getShell().computeTrim(0, 0, 0, 0).width; Point constrainedSize = getShell() .computeSize(constrains.x - trimWidth, SWT.DEFAULT, true); int width = min(preferedSize.x, constrainedSize.x); int height = max(preferedSize.y, constrainedSize.y); return new Point(width, height); } private final static ICompletionProposal[] NO_PROPOSAL = new ICompletionProposal[0]; /** * Create content of the hover. This is called after * the input has been set. */ protected void deferredCreateContent() { createAnnotationInformation(fParent); setColorAndFont(fParent, fParent.getForeground(), fParent.getBackground(), CeylonPlugin.getHoverFont()); ICompletionProposal[] existingProposals = fInput.getProposals(); quickFixesRetrieved.set(false); if (existingProposals == null) { Composite quickFixProgressGroup = createQuickFixProgressBar(); createQuickFixesRetrievalJob(fParent, quickFixProgressGroup) .schedule(); } else if (existingProposals.length > 0) { createCompletionProposalsControl(fParent, existingProposals); } fParent.layout(true); } private Job createQuickFixesRetrievalJob( final Composite currentParent, final Composite quickFixProgressGroup) { Job completionProposalsJob = new Job("Retrieving Fixes") { @Override protected IStatus run(IProgressMonitor monitor) { AnnotationInfo info = fInput; final ICompletionProposal[] proposals; if (getTypecheckedRootNode(info) == null) { proposals = NO_PROPOSAL; } else { proposals = getAnnotationInfo() .getCompletionProposals(); fInput.setProposals(proposals); } if (fParent == currentParent && ! fParent.isDisposed()) { Display display = fParent.getDisplay(); if (display != null) { display.asyncExec(new Runnable() { public void run() { if (fParent.isVisible()) { if (!quickFixProgressGroup.isDisposed()) { quickFixProgressGroup.dispose(); } if (proposals.length > 0 && quickFixesRetrieved .compareAndSet(false, true)) { createCompletionProposalsControl(fParent, proposals); } fParent.layout(true); fParent.pack(true); Point sizeHint = computeSizeHint(); setSize(sizeHint.x, sizeHint.y); } } }); } } return Status.OK_STATUS; } }; completionProposalsJob.setSystem(true); completionProposalsJob.setRule(retrievingQuickFixesRule); return completionProposalsJob; } private Composite createQuickFixProgressBar() { Composite quickFixProgressGroup = new Composite(fParent, SWT.FILL); GridData qk1 = new GridData(SWT.FILL, SWT.FILL, true, true); qk1.horizontalSpan = 2; quickFixProgressGroup.setLayoutData(qk1); GridLayout layout2 = new GridLayout(2, true); layout2.marginHeight = 0; layout2.marginWidth = 10; layout2.verticalSpacing = 2; quickFixProgressGroup.setLayout(layout2); Label progressLabel = new Label(quickFixProgressGroup, SWT.NONE); progressLabel.setText("Preparing quick fixes..."); new ProgressBar(quickFixProgressGroup, SWT.INDETERMINATE | SWT.FILL); setColorAndFont(fParent, fParent.getForeground(), fParent.getBackground(), CeylonPlugin.getHoverFont()); return quickFixProgressGroup; } private void setColorAndFont(Control control, Color foreground, Color background, Font font) { control.setForeground(foreground); control.setBackground(background); control.setFont(font); if (control instanceof Composite) { Composite composite = (Composite) control; Control[] children = composite.getChildren(); for (int i=0; i<children.length; i++) { setColorAndFont(children[i], foreground, background, font); } } } private void createAnnotationInformation(Composite parent) { Composite composite = new Composite(parent, SWT.NONE); GridData gd1 = new GridData(SWT.FILL, SWT.TOP, true, false); composite.setLayoutData(gd1); GridLayout layout = new GridLayout(2, false); layout.marginHeight = 15; layout.marginWidth = 15; layout.horizontalSpacing = 4; layout.marginRight = 5; composite.setLayout(layout); Annotation[] annotations = getAnnotationInfo() .getAnnotationPositions() .keySet() .toArray(new Annotation[0]); Arrays.sort(annotations, createAnnotationComparator()); for (final Annotation annotation: annotations) { final Canvas canvas = new Canvas(composite, SWT.NO_FOCUS); GridData gd2 = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false); gd2.widthHint = 17; gd2.heightHint = 16; canvas.setLayoutData(gd2); canvas.addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent e) { e.gc.setFont(null); fMarkerAnnotationAccess.paint(annotation, e.gc, canvas, new Rectangle(0, 0, 16, 16)); } }); GridData gd3 = new GridData(SWT.FILL, SWT.FILL, true, true); if (annotation instanceof RefinementAnnotation) { Link link = new Link(composite, SWT.NONE); String text = annotation.getText() .replaceFirst(" ", " <a>") + "</a>"; link.setText(text); link.addSelectionListener(new SelectionListener() { @Override public void widgetSelected(SelectionEvent e) { RefinementAnnotation ra = (RefinementAnnotation) annotation; CeylonEditor editor = getAnnotationInfo() .getEditor(); ra.gotoRefinedDeclaration(editor); } @Override public void widgetDefaultSelected(SelectionEvent e) {} }); } /*else if (annotation instanceof CeylonRangeIndicator) { StyledText text = new StyledText(composite, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY); text.setLayoutData(gd3); String message = annotation.getText(); if (message!=null && !message.isEmpty()) { CeylonRangeIndicator cia = (CeylonRangeIndicator) annotation; StyledString styledString = cia.getStyledString(); text.setText(styledString.getString()); text.setStyleRanges(styledString.getStyleRanges()); } }*/ else if (annotation instanceof CeylonInitializerAnnotation) { StyledText text = new StyledText(composite, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY); text.setLayoutData(gd3); String message = annotation.getText(); if (message!=null && !message.isEmpty()) { CeylonInitializerAnnotation cia = (CeylonInitializerAnnotation) annotation; StyledString styledString = cia.getStyledString(); text.setText(styledString.getString()); text.setStyleRanges(styledString.getStyleRanges()); } } else { StyledText text = new StyledText(composite, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY); text.setLayoutData(gd3); String message = annotation.getText(); if (message!=null && !message.isEmpty()) { message = escaping_.get_() .toInitialUppercase(message); StyledString styled = Highlights.styleProposal( message, true, true); text.setText(styled.getString()); StyleRange[] styleRanges = styled.getStyleRanges(); Font editorFont = //monospaced font for identifiers CeylonPlugin.getEditorFont(); Font hoverFont = //regular font for message text CeylonPlugin.getHoverFont(); FontData monospaceFontData = editorFont.getFontData()[0]; Display display = Display.getDefault(); Shell activeShell = display.getActiveShell(); if (activeShell!=null) { GC gc = new GC(activeShell); Font font = gc.getFont(); gc.setFont(hoverFont); int hoverFontHeight = gc.getFontMetrics() .getAscent(); gc.setFont(editorFont); int monospaceFontHeight = gc.getFontMetrics() .getAscent(); gc.setFont(font); int height = monospaceFontData.getHeight() * hoverFontHeight / monospaceFontHeight; Font result = new Font(display, monospaceFontData.getName(), height, monospaceFontData.getStyle()); for (StyleRange range: styleRanges) { range.font = result; } } text.setStyleRanges(styleRanges); } } } } Comparator<Annotation> createAnnotationComparator() { return new AnnotationComparator(); } private void createCompletionProposalsControl( Composite parent, ICompletionProposal[] proposals) { Composite composite = new Composite(parent, SWT.NONE); GridData gd1 = new GridData(SWT.FILL, SWT.FILL, true, true); composite.setLayoutData(gd1); GridLayout layout2 = new GridLayout(1, false); layout2.marginHeight = 0; layout2.marginWidth = 10; layout2.verticalSpacing = 2; composite.setLayout(layout2); // Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); // GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, false); // separator.setLayoutData(gridData); Label quickFixLabel = new Label(composite, SWT.NONE); GridData gd2 = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); gd2.horizontalIndent = 4; quickFixLabel.setLayoutData(gd2); String text; if (proposals.length == 1) { text = "1 quick fix available:"; } else { text = proposals.length + " quick fixes available:"; } quickFixLabel.setText(text); setColorAndFont(composite, parent.getForeground(), parent.getBackground(), CeylonPlugin.getHoverFont()); createCompletionProposalsList(composite, proposals); } private void createCompletionProposalsList( Composite parent, ICompletionProposal[] proposals) { final ScrolledComposite scrolledComposite = new ScrolledComposite(parent, SWT.V_SCROLL | SWT.H_SCROLL); GridData gd1 = new GridData(SWT.FILL, SWT.FILL, true, true); scrolledComposite.setLayoutData(gd1); scrolledComposite.setExpandVertical(false); scrolledComposite.setExpandHorizontal(false); Composite composite = new Composite(scrolledComposite, SWT.NONE); GridData gd2 = new GridData(SWT.FILL, SWT.FILL, true, true); composite.setLayoutData(gd2); GridLayout layout = new GridLayout(3, false); layout.verticalSpacing = 2; composite.setLayout(layout); final Link[] links = new Link[proposals.length]; for (int i=0; i<proposals.length; i++) { Label indent = new Label(composite, SWT.NONE); GridData gridData1 = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); gridData1.widthHint = 0; indent.setLayoutData(gridData1); links[i] = createCompletionProposalLink(composite, proposals[i]); } scrolledComposite.setContent(composite); setColorAndFont(scrolledComposite, parent.getForeground(), parent.getBackground(), CeylonPlugin.getHoverFont()); Point contentSize = composite.computeSize( SWT.DEFAULT, SWT.DEFAULT); composite.setSize(contentSize); Point constraints = getSizeConstraints(); if (constraints != null && contentSize.x < constraints.x) { ScrollBar horizontalBar = scrolledComposite.getHorizontalBar(); int scrollBarHeight; if (horizontalBar == null) { Point scrollSize = scrolledComposite.computeSize( SWT.DEFAULT, SWT.DEFAULT); scrollBarHeight = scrollSize.y - contentSize.y; } else { scrollBarHeight = horizontalBar.getSize().y; } gd1.heightHint = contentSize.y - scrollBarHeight; } fFocusControl = links[0]; for (int i=0; i<links.length; i++) { final int index = i; final Link link = links[index]; link.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { switch (e.keyCode) { case SWT.ARROW_DOWN: if (index + 1 < links.length) { links[index + 1].setFocus(); } break; case SWT.ARROW_UP: if (index > 0) { links[index - 1].setFocus(); } break; default: break; } } @Override public void keyReleased(KeyEvent e) {} }); link.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { int currentPosition = scrolledComposite.getOrigin().y; int height = scrolledComposite.getSize().y; int linkPosition = link.getLocation().y; if (linkPosition < currentPosition) { if (linkPosition < 10) { linkPosition= 0; } scrolledComposite.setOrigin(0, linkPosition); } else if (linkPosition + 20 > currentPosition + height) { scrolledComposite.setOrigin(0, linkPosition - height + link.getSize().y); } } @Override public void focusLost(FocusEvent e) {} }); } } private Link createCompletionProposalLink( Composite parent, final ICompletionProposal proposal) { Label proposalImage = new Label(parent, SWT.NONE); GridData gd1 = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); proposalImage.setLayoutData(gd1); Image image = proposal.getImage(); if (image != null) { proposalImage.setImage(image); proposalImage.addMouseListener(new MouseListener() { @Override public void mouseDoubleClick(MouseEvent e) {} @Override public void mouseDown(MouseEvent e) {} @Override public void mouseUp(MouseEvent e) { if (e.button == 1) { apply(proposal, fInput.getViewer()); } } }); } Link proposalLink = new Link(parent, SWT.WRAP); GridData gd2 = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); proposalLink.setFont(CeylonPlugin.getHoverFont()); proposalLink.setLayoutData(gd2); proposalLink.setText("<a>" + proposal.getDisplayString() .replace("&", "&&") + "</a>"); proposalLink.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { apply(proposal, fInput.getViewer()); } }); return proposalLink; } private void apply(ICompletionProposal p, ITextViewer viewer) { //Focus needs to be in the text viewer, //otherwise linked mode does not work dispose(); IRewriteTarget target = null; try { IDocument document = viewer.getDocument(); if (viewer instanceof ITextViewerExtension) { ITextViewerExtension extension = (ITextViewerExtension) viewer; target = extension.getRewriteTarget(); } if (target != null) target.beginCompoundChange(); // if (p instanceof ICompletionProposalExtension2) { // ICompletionProposalExtension2 e= (ICompletionProposalExtension2) p; // e.apply(viewer, (char) 0, SWT.NONE, offset); // } else if (p instanceof ICompletionProposalExtension) { // ICompletionProposalExtension e= (ICompletionProposalExtension) p; // e.apply(document, (char) 0, offset); // } else { p.apply(document); // } Point selection = p.getSelection(document); if (selection != null) { int x = selection.x; int y = selection.y; viewer.setSelectedRange(x, y); viewer.revealRange(x, y); } } finally { if (target != null) target.endCompoundChange(); } } }