blob: ea3097c0850a6b695b32d250ef6f2ce334536e64 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java.hover;
import java.io.IOException;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.part.IWorkbenchPartOrientation;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;
import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.core.manipulation.util.Strings;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.TokenScanner;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.jdt.internal.ui.javaeditor.JavaSourceViewer;
import org.eclipse.jdt.internal.ui.text.JavaCodeReader;
import org.eclipse.jdt.internal.ui.text.JavaPairMatcher;
/**
* Provides as hover info the source of the selected JavaElement, or the source near the matching
* opening curly brace.
*/
public class JavaSourceHover extends AbstractJavaEditorTextHover {
/**
* The upward shift in location in lines for the bracket hover.
*
* @since 3.8
*/
private int fUpwardShiftInLines;
/**
* The status text for the bracket hover.
*
* @since 3.8
*/
private String fBracketHoverStatus;
/**
* The hovered Java element to get the source.
*
* @since 3.14
*/
private IJavaElement fJavaElement;
class JavaSourceInformationInput {
private IJavaElement fElement;
private String fHoverInfo;
public JavaSourceInformationInput(IJavaElement javaElement, String hoverInfo) {
fElement= javaElement;
fHoverInfo= hoverInfo;
}
public IJavaElement getJavaElement() {
return fElement;
}
public String getHoverInfo() {
return fHoverInfo;
}
}
@Override
public Object getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion) {
String hoverInfoString= getHoverInfo(textViewer, hoverRegion);
if (hoverInfoString == null) {
return null;
}
if (fJavaElement == null) {
return hoverInfoString;
}
return new JavaSourceInformationInput(fJavaElement, hoverInfoString);
}
@Override
@Deprecated
public String getHoverInfo(ITextViewer textViewer, IRegion region) {
IJavaElement[] result= getJavaElementsAt(textViewer, region);
fJavaElement= null;
fUpwardShiftInLines= 0;
fBracketHoverStatus= null;
if (result == null || result.length == 0) {
return getBracketHoverInfo(textViewer, region);
}
if (result.length > 1)
return null;
fJavaElement= result[0];
if ((fJavaElement instanceof IMember || fJavaElement instanceof ILocalVariable || fJavaElement instanceof ITypeParameter) && fJavaElement instanceof ISourceReference) {
try {
String source= ((ISourceReference) fJavaElement).getSource();
String[] sourceLines= getTrimmedSource(source, fJavaElement);
if (sourceLines == null)
return null;
String delim= StubUtility.getLineDelimiterUsed(fJavaElement);
source= Strings.concatenate(sourceLines, delim);
return source;
} catch (JavaModelException ex) {
//do nothing
}
}
return null;
}
private String getBracketHoverInfo(final ITextViewer textViewer, IRegion region) {
boolean isElsePart= false;
IEditorPart editor= getEditor();
ITypeRoot editorInput= getEditorInputJavaElement();
if (!(editor instanceof JavaEditor) || editorInput == null) {
return null;
}
int offset= region.getOffset();
IDocument document= textViewer.getDocument();
if (document == null)
return null;
try {
char c= document.getChar(offset);
if (c != '}')
return null;
JavaPairMatcher matcher= ((JavaEditor) editor).getBracketMatcher();
if (matcher == null)
return null;
IRegion match= matcher.match(document, offset);
if (match == null)
return null;
String delim= StubUtility.getLineDelimiterUsed(editorInput);
CompilationUnit ast= SharedASTProviderCore.getAST(editorInput, SharedASTProviderCore.WAIT_NO, null);
if (ast == null)
return null;
ASTNode bracketNode= NodeFinder.perform(ast, match.getOffset(),
match.getLength());
if (bracketNode == null)
return null;
ASTNode node;
ASTNode parent= bracketNode.getParent();
if (parent instanceof IfStatement) {
IfStatement parentIfStmt= (IfStatement) parent;
if ((parentIfStmt.getElseStatement() != null && ASTNodes.getInclusiveEnd(parentIfStmt.getElseStatement()) == offset) // if [else if]* else
|| (parentIfStmt.getLocationInParent() == IfStatement.ELSE_STATEMENT_PROPERTY && ASTNodes.getInclusiveEnd(parentIfStmt.getThenStatement()) == offset)) { // if [else if]+ else?
isElsePart= true;
while (parent.getLocationInParent() == IfStatement.ELSE_STATEMENT_PROPERTY) {
parent= parent.getParent();
}
}
}
if (bracketNode instanceof Block && !(parent instanceof Block) && !(parent instanceof SwitchStatement)) {
node= parent;
} else {
node= bracketNode;
}
int nodeStart;
int nodeLength;
if (node instanceof BodyDeclaration) {
BodyDeclaration declaration= (BodyDeclaration) node;
Javadoc javadoc= declaration.getJavadoc();
int lengthOfJavadoc= javadoc == null ? 0 : javadoc.getLength() +
delim.length();
nodeStart= node.getStartPosition() + lengthOfJavadoc;
nodeLength= node.getLength() - lengthOfJavadoc;
} else {
nodeStart= node.getStartPosition();
nodeLength= ASTNodes.getExclusiveEnd(bracketNode) - nodeStart;
}
int line1= document.getLineOfOffset(nodeStart);
int sourceOffset= document.getLineOffset(line1);
int line2= document.getLineOfOffset(nodeStart + nodeLength);
int hoveredLine= document.getLineOfOffset(offset);
if (line2 > hoveredLine)
line2= hoveredLine;
//check if line1 is visible
final int[] topIndex= new int[1];
StyledText textWidget= textViewer.getTextWidget();
if (textWidget == null)
return null;
Display display;
try {
display= textWidget.getDisplay();
} catch (SWTException ex) {
if (ex.code == SWT.ERROR_WIDGET_DISPOSED)
return null;
else
throw ex;
}
display.syncExec(new Runnable() {
@Override
public void run() {
topIndex[0]= textViewer.getTopIndex();
}
});
int topLine= topIndex[0];
if (topLine == -1)
return null;
int noOfSourceLines;
IRegion endLine;
int skippedLines= 0;
int wLine1= ((JavaSourceViewer) textViewer).modelLine2WidgetLine(line1);
int wLine2= ((JavaSourceViewer) textViewer).modelLine2WidgetLine(line2);
if ((line1 < topLine) || (wLine1 != -1 && (wLine2 - wLine1 != line2 - line1))) {
// match not visible or content is folded - see bug 399997
if (isElsePart) {
return getBracketHoverInfo((IfStatement) node, bracketNode, document, editorInput, delim); // see bug 377141, 201850
}
noOfSourceLines= 3;
if ((line2 - line1) < noOfSourceLines) {
noOfSourceLines= line2 - line1;
}
skippedLines= Math.abs(line2 - line1 - noOfSourceLines);
if (skippedLines == 1) {
noOfSourceLines++;
skippedLines= 0;
}
endLine= document.getLineInformation(line1 + noOfSourceLines);
fUpwardShiftInLines= noOfSourceLines;
if (skippedLines > 0) {
fBracketHoverStatus= Messages.format(JavaHoverMessages.JavaSourceHover_skippedLines, Integer.valueOf(skippedLines));
}
} else {
noOfSourceLines= line2 - line1;
endLine= document.getLineInformation(line2);
fUpwardShiftInLines= line2 - line1;
}
if (fUpwardShiftInLines == 0)
return null;
int sourceLength= (endLine.getOffset() + endLine.getLength()) - sourceOffset;
String source= document.get(sourceOffset, sourceLength);
String[] sourceLines= getTrimmedSource(source, editorInput);
if (sourceLines == null)
return null;
String[] str= new String[noOfSourceLines];
System.arraycopy(sourceLines, 0, str, 0, noOfSourceLines);
source= Strings.concatenate(str, delim);
if (skippedLines > 0) {
source= source.concat(delim).concat(JavaHoverMessages.JavaSourceHover_skippedLinesSymbol);
fUpwardShiftInLines++;
}
return source;
} catch (BadLocationException e) {
JavaPlugin.log(e);
return null;
}
}
/**
* Creates the hover text for 'else if' or 'else' closing bracket in 'if [else if]+ else?' or
* 'if [else if]* else' cases by stitching together all the headers when the beginning of the
* first 'if' is not visible in the text editor.
*
* @param ifNode the first 'if' node in the structure
* @param bracketNode the node at whose closing bracket the hover text is required
* @param document the input document of the text viewer on which the hover popup should be
* shown
* @param editorInput the editor's input as {@link ITypeRoot}
* @param delim the line delimiter used for the editorInput
* @return the hover text for 'else if' or 'else' closing bracket in 'if [else if]+ else?' or
* 'if [else if]* else' cases respectively
* @throws BadLocationException if an attempt has been performed to access a non-existing
* position in the document
* @since 3.9
*/
private String getBracketHoverInfo(IfStatement ifNode, ASTNode bracketNode, final IDocument document, final ITypeRoot editorInput, final String delim) throws BadLocationException {
int totalSkippedLines= 0;
String hoverText= null;
Statement currentStatement= ifNode.getThenStatement();
int nodeStart= ifNode.getStartPosition();
while (currentStatement != null) {
int nodeLength= ASTNodes.getExclusiveEnd(currentStatement) - nodeStart;
int line1= document.getLineOfOffset(nodeStart);
int sourceOffset= document.getLineOffset(line1);
int line2= document.getLineOfOffset(nodeStart + nodeLength);
int line3= line2;
if (currentStatement != bracketNode && ifNode != null && ifNode.getElseStatement() != null) {
int elseStartOffset= getNextElseOffset(ifNode.getThenStatement(), editorInput);
if (elseStartOffset != -1) {
line3= document.getLineOfOffset(elseStartOffset); // next 'else'
}
}
int noOfTotalLines= (line2 == line3) ? (line2 - line1) : (line2 - line1 + 1);
int noOfSourceLines= 3;
if (noOfTotalLines < noOfSourceLines) {
noOfSourceLines= noOfTotalLines;
}
int noOfSkippedLines= noOfTotalLines - noOfSourceLines;
if (noOfSkippedLines == 1) {
noOfSourceLines++;
noOfSkippedLines= 0;
}
if (noOfSourceLines > 0) {
IRegion endLine= document.getLineInformation(line1 + noOfSourceLines - 1);
int sourceLength= (endLine.getOffset() + endLine.getLength()) - sourceOffset;
String source= document.get(sourceOffset, sourceLength);
String[] sourceLines= getTrimmedSource(source, editorInput);
if (sourceLines == null)
return null;
source= Strings.concatenate(sourceLines, delim);
if (noOfSkippedLines > 0) {
source= source.concat(delim).concat(JavaHoverMessages.JavaSourceHover_skippedLinesSymbol);
fUpwardShiftInLines++;
}
fUpwardShiftInLines+= noOfSourceLines;
totalSkippedLines+= noOfSkippedLines;
if (hoverText == null) {
hoverText= source;
} else {
hoverText= hoverText.concat(delim).concat(source);
}
}
// advance currentStatement to the next 'else if' or 'else' statement; set it to null when no further processing is required
// advance ifNode to the 'if' in next 'else if'; set it to null if 'else' is reached
if (currentStatement != bracketNode) {
Statement thenStatement= ifNode.getThenStatement();
Statement nextStatement= ifNode.getElseStatement();
if (nextStatement instanceof IfStatement) {
currentStatement= ((IfStatement) nextStatement).getThenStatement();
ifNode= (IfStatement) nextStatement;
} else {
currentStatement= nextStatement;
ifNode= null;
}
// update nodeStart to next 'else' start offset
int nextStartOffset= getNextElseOffset(thenStatement, editorInput);
if (nextStartOffset != -1) {
nodeStart= nextStartOffset;
} else {
nodeStart= nextStatement.getStartPosition();
}
} else {
currentStatement= null;
}
}
if (fUpwardShiftInLines == 0)
return null;
if ((totalSkippedLines) > 0) {
fBracketHoverStatus= Messages.format(JavaHoverMessages.JavaSourceHover_skippedLines, Integer.valueOf(totalSkippedLines));
}
return hoverText;
}
private int getNextElseOffset(Statement then, ITypeRoot editorInput) {
int thenEnd= ASTNodes.getExclusiveEnd(then);
try {
TokenScanner scanner= new TokenScanner(editorInput);
return scanner.getNextStartOffset(thenEnd, true);
} catch (CoreException e) {
// ignore
}
return -1;
}
/**
* Returns the trimmed source lines.
*
* @param source the source string, could be <code>null</code>
* @param javaElement the java element
* @return the trimmed source lines or <code>null</code>
*/
private String[] getTrimmedSource(String source, IJavaElement javaElement) {
if (source == null)
return null;
source= removeLeadingComments(source);
String[] sourceLines= Strings.convertIntoLines(source);
Strings.trimIndentation(sourceLines, javaElement.getJavaProject());
return sourceLines;
}
private String removeLeadingComments(String source) {
final JavaCodeReader reader= new JavaCodeReader();
IDocument document= new Document(source);
int i;
try {
reader.configureForwardReader(document, 0, document.getLength(), true, false);
int c= reader.read();
while (c != -1 && (c == '\r' || c == '\n')) {
c= reader.read();
}
i= reader.getOffset();
reader.close();
} catch (IOException ex) {
i= 0;
} finally {
try {
reader.close();
} catch (IOException ex) {
JavaPlugin.log(ex);
}
}
if (i < 0)
return source;
return source.substring(i);
}
/*
* @see org.eclipse.jface.text.ITextHoverExtension#getHoverControlCreator()
* @since 3.0
*/
@Override
public IInformationControlCreator getHoverControlCreator() {
if (fUpwardShiftInLines > 0)
return createInformationControlCreator(false, fBracketHoverStatus, true);
else
return createInformationControlCreator(false, EditorsUI.getTooltipAffordanceString(), true);
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.hover.AbstractJavaEditorTextHover#getInformationPresenterControlCreator()
* @since 3.0
*/
@Override
public IInformationControlCreator getInformationPresenterControlCreator() {
if (fUpwardShiftInLines > 0)
return createInformationControlCreator(false, fBracketHoverStatus, true);
else
return createInformationControlCreator(true, EditorsUI.getTooltipAffordanceString(), true);
}
/**
* Returns the information control creator.
*
* @param isResizable <code>true</code> if resizable
* @param statusFieldText the text to be used in the optional status field or <code>null</code>
* if the status field should be hidden
* @param doShiftUp <code>true</code> iff {@link #fUpwardShiftInLines} should be considered
* @return the information control creator
* @since 3.8
*/
private IInformationControlCreator createInformationControlCreator(final boolean isResizable, final String statusFieldText, final boolean doShiftUp) {
return new IInformationControlCreator() {
@Override
public IInformationControl createInformationControl(final Shell parent) {
final IEditorPart editor= getEditor();
int orientation= SWT.NONE;
if (editor instanceof IWorkbenchPartOrientation)
orientation= ((IWorkbenchPartOrientation) editor).getOrientation();
return new SourceViewerInformationControl(parent, isResizable, orientation, statusFieldText) {
@Override
public void setLocation(Point location) {
Point loc= location;
if (doShiftUp && fUpwardShiftInLines > 0) {
Point size= super.computeSizeConstraints(0, fUpwardShiftInLines + 1);
//bracket hover is rendered above '}'
int y= location.y - size.y - 5; //AbstractInformationControlManager.fMarginY = 5
Rectangle trim= computeTrim();
loc= new Point(location.x + trim.x - getViewer().getTextWidget().getLeftMargin(), y - trim.height - trim.y);
}
super.setLocation(loc);
}
@Override
public Point computeSizeConstraints(int widthInChars, int heightInChars) {
if (doShiftUp && fUpwardShiftInLines > 0) {
Point sizeConstraints= super.computeSizeConstraints(widthInChars, heightInChars);
return new Point(sizeConstraints.x, 0); //set height as 0 to ensure selection of bottom anchor in AbstractInformationControlManager.computeInformationControlLocation(...)
} else {
return super.computeSizeConstraints(widthInChars, heightInChars);
}
}
@Override
public void setSize(int width, int height) {
if (doShiftUp && fUpwardShiftInLines != 0) {
//compute the correct height of hover, this was set to 0 in computeSizeConstraints(..)
Point size= super.computeSizeConstraints(0, fUpwardShiftInLines);
Rectangle trim= computeTrim();
super.setSize(width, size.y + trim.height - trim.y);
} else {
super.setSize(width, height);
}
}
@Override
public IInformationControlCreator getInformationPresenterControlCreator() {
if (doShiftUp && fUpwardShiftInLines > 0) {
// Hack: We don't wan't to have auto-enrichment when the mouse moves into the hover,
// but we do want F2 to persist the hover. The framework has no way to distinguish the
// two requests, so we have to implement this aspect.
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
if ("canMoveIntoInformationControl".equals(element.getMethodName()) //$NON-NLS-1$
&& "org.eclipse.jface.text.AbstractHoverInformationControlManager".equals(element.getClassName())) //$NON-NLS-1$
return null; //do not enrich bracket hover
}
return JavaSourceHover.this.createInformationControlCreator(isResizable && !isResizable, statusFieldText, false);
} else {
return super.getInformationPresenterControlCreator();
}
}
};
}
};
}
}