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