1 /*******************************************************************************
2  * Copyright (c) 2007, 2015 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.jdt.internal.debug.ui.actions;
15 
16 import org.eclipse.core.runtime.CoreException;
17 import org.eclipse.core.runtime.IAdapterManager;
18 import org.eclipse.core.runtime.Platform;
19 import org.eclipse.debug.core.DebugEvent;
20 import org.eclipse.debug.core.DebugException;
21 import org.eclipse.debug.core.DebugPlugin;
22 import org.eclipse.debug.core.IDebugEventSetListener;
23 import org.eclipse.debug.core.model.IStackFrame;
24 import org.eclipse.debug.core.model.IThread;
25 import org.eclipse.debug.ui.actions.IRunToLineTarget;
26 import org.eclipse.jdt.core.ICodeAssist;
27 import org.eclipse.jdt.core.IJavaElement;
28 import org.eclipse.jdt.core.IMember;
29 import org.eclipse.jdt.core.IMethod;
30 import org.eclipse.jdt.core.IType;
31 import org.eclipse.jdt.core.JavaCore;
32 import org.eclipse.jdt.core.JavaModelException;
33 import org.eclipse.jdt.core.ToolFactory;
34 import org.eclipse.jdt.core.compiler.IScanner;
35 import org.eclipse.jdt.core.compiler.ITerminalSymbols;
36 import org.eclipse.jdt.core.compiler.InvalidInputException;
37 import org.eclipse.jdt.debug.core.IJavaStackFrame;
38 import org.eclipse.jdt.debug.core.IJavaThread;
39 import org.eclipse.jdt.internal.debug.ui.EvaluationContextManager;
40 import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin;
41 import org.eclipse.jdt.ui.JavaUI;
42 import org.eclipse.jface.text.BadLocationException;
43 import org.eclipse.jface.text.IDocument;
44 import org.eclipse.jface.text.IRegion;
45 import org.eclipse.jface.text.ITextSelection;
46 import org.eclipse.ui.IEditorInput;
47 import org.eclipse.ui.IEditorPart;
48 import org.eclipse.ui.IWorkbenchPage;
49 import org.eclipse.ui.texteditor.IDocumentProvider;
50 import org.eclipse.ui.texteditor.IEditorStatusLine;
51 import org.eclipse.ui.texteditor.ITextEditor;
52 
53 /**
54  * Utility class for aiding step into selection actions and hyper-linking
55  *
56  * @see StepIntoSelectionActionDelegate
57  * @see StepIntoSelectionHyperlinkDetector
58  *
59  * @since 3.3
60  */
61 public class StepIntoSelectionUtils {
62 
63 
64 	/**
65      * gets the <code>IJavaElement</code> from the editor input
66      * @param input the current editor input
67      * @return the corresponding <code>IJavaElement</code>
68      */
getJavaElement(IEditorInput input)69     public static IJavaElement getJavaElement(IEditorInput input) {
70     	IJavaElement je = JavaUI.getEditorInputJavaElement(input);
71     	if(je != null) {
72     		return je;
73     	}
74     	return JavaUI.getWorkingCopyManager().getWorkingCopy(input);
75     }
76 
77     /**
78      * Returns the <code>IMethod</code> from the given selection within the given <code>IJavaElement</code>,
79      * or <code>null</code> if the selection does not container or is not an <code>IMethod</code>
80      * @param selection
81      * @param element
82      * @return the corresponding <code>IMethod</code> from the selection within the provided <code>IJavaElement</code>
83      * @throws JavaModelException
84      */
getMethod(ITextSelection selection, IJavaElement element)85     public static IMethod getMethod(ITextSelection selection, IJavaElement element) throws JavaModelException {
86     	if(element != null && element instanceof ICodeAssist) {
87     		return resolveMethod(selection.getOffset(), selection.getLength(), (ICodeAssist)element);
88     	}
89     	return null;
90     }
91 
92 	/**
93 	 * @param offset selection offset
94 	 * @param length selection length
95 	 * @param codeAssist context
96 	 * @return the method at the given position, or <code>null</code> if no method could be resolved
97 	 * @throws JavaModelException
98 	 */
resolveMethod(int offset, int length, ICodeAssist codeAssist)99 	private static IMethod resolveMethod(int offset, int length, ICodeAssist codeAssist) throws JavaModelException {
100 		IJavaElement[] elements = codeAssist.codeSelect(offset, length);
101 		for (int i = 0; i < elements.length; i++) {
102 			if (elements[i] instanceof IMethod) {
103 				return (IMethod)elements[i];
104 			}
105 		}
106 		return null;
107 	}
108 
109 	/**
110 	 * @param offset
111 	 * @param activeEditor
112 	 * @param element
113 	 * @return the first method found at or after <code>offset</code> on the same line
114 	 * @throws JavaModelException
115 	 */
getFirstMethodOnLine(int offset, IEditorPart activeEditor, IJavaElement element)116 	public static IMethod getFirstMethodOnLine(int offset, IEditorPart activeEditor, IJavaElement element) throws JavaModelException {
117 		if (! (activeEditor instanceof ITextEditor) || ! (element instanceof ICodeAssist)) {
118 			return null;
119 		}
120 		ITextEditor editor = (ITextEditor)activeEditor;
121 		IDocumentProvider documentProvider = editor.getDocumentProvider();
122 		if (documentProvider == null) {
123 			return null;
124 		}
125 		IDocument document = documentProvider.getDocument(editor.getEditorInput());
126 		if (document == null) {
127 			return null;
128 		}
129 		try {
130 			IRegion lineInfo = document.getLineInformationOfOffset(offset);
131 			String line = document.get(lineInfo.getOffset(), lineInfo.getLength());
132 			IScanner scanner = ToolFactory.createScanner(false, false, false, null, JavaCore.VERSION_1_5);
133 			scanner.setSource(line.toCharArray());
134 			scanner.resetTo(offset - lineInfo.getOffset(), lineInfo.getLength());
135 			int token = scanner.getNextToken();
136 			while (token != ITerminalSymbols.TokenNameEOF) {
137 				if (token == ITerminalSymbols.TokenNameIdentifier) {
138 					int methodStart = scanner.getCurrentTokenStartPosition();
139 					token = scanner.getNextToken();
140 					if (token == ITerminalSymbols.TokenNameLPAREN) {
141 						return resolveMethod(lineInfo.getOffset() + methodStart, 0, (ICodeAssist)element);
142 					}
143 				}
144 				else {
145 					token = scanner.getNextToken();
146 				}
147 			}
148 		}
149 		catch (BadLocationException e) {
150 			return null;
151 		}
152 		catch (InvalidInputException e) {
153 			return null;
154 		}
155 		return null;
156 	}
157 
158 	/**
159 	 * Steps into the selection described by the given {@link IRegion}
160 	 *
161 	 * @param region the region of the selection or <code>null</code> if we should use the page's selection service to compute
162 	 * the selection
163 	 *
164 	 * @since 3.6.200
165 	 */
stepIntoSelection(ITextSelection selection)166 	public static void stepIntoSelection(ITextSelection selection) {
167 		IWorkbenchPage page = JDIDebugUIPlugin.getActiveWorkbenchWindow().getActivePage();
168 		if(page != null) {
169 			IEditorPart editor = page.getActiveEditor();
170 			if(editor instanceof ITextEditor) {
171 				IJavaStackFrame frame = EvaluationContextManager.getEvaluationContext(editor);
172 				if (frame == null || !frame.isSuspended()) {
173 					// no longer suspended - unexpected
174 					return;
175 				}
176 				if(selection == null) {
177 					//grab it from the provider, either the passed region was null or we failed to get it
178 					selection = (ITextSelection)editor.getEditorSite().getSelectionProvider().getSelection();
179 				}
180 				try {
181 					IJavaElement javaElement= StepIntoSelectionUtils.getJavaElement(editor.getEditorInput());
182 					IMethod method = StepIntoSelectionUtils.getMethod(selection, javaElement);
183 					if (method == null) {
184 						method = StepIntoSelectionUtils.getFirstMethodOnLine(selection.getOffset(), editor, javaElement);
185 					}
186 					IType callingType = getType(selection);
187 					if (method == null || callingType == null) {
188 						return;
189 					}
190 					int lineNumber = frame.getLineNumber();
191 		            String callingTypeName = stripInnerNamesAndParameterType(callingType.getFullyQualifiedName());
192 		            String frameName = stripInnerNamesAndParameterType(frame.getDeclaringTypeName());
193 					// debug line numbers are 1 based, document line numbers are 0 based
194 					if (selection.getStartLine() == (lineNumber - 1) && callingTypeName.equals(frameName)) {
195 		                doStepIn(editor, frame, method);
196 					} else {
197 						// not on current line
198 						runToLineBeforeStepIn(editor, callingTypeName, selection, frame.getThread(), method);
199 						return;
200 					}
201 				}
202 				catch (DebugException e) {
203 					showErrorMessage(editor, e.getStatus().getMessage());
204 					return;
205 				}
206 				catch(JavaModelException jme) {
207 					showErrorMessage(editor, jme.getStatus().getMessage());
208 					return;
209 				}
210 			}
211 		}
212 	}
213 
214 	/**
215 	 * When the user chooses to "step into selection" on a line other than
216 	 * the currently executing one, first perform a "run to line" to get to
217 	 * the desired location, then perform a "step into selection."
218 	 */
runToLineBeforeStepIn(final IEditorPart editor, final String typeName, ITextSelection textSelection, final IThread thread, final IMethod method)219 	static void runToLineBeforeStepIn(final IEditorPart editor, final String typeName, ITextSelection textSelection, final IThread thread, final IMethod method) {
220 		final int line = textSelection.getStartLine() + 1;
221 		if (typeName == null || line == -1) {
222 			return;
223 		}
224 		// see bug 65489 - get the run-to-line adapter from the editor
225 		IRunToLineTarget runToLineAction = null;
226 		if (editor != null) {
227 			runToLineAction  = editor.getAdapter(IRunToLineTarget.class);
228 			if (runToLineAction == null) {
229 				IAdapterManager adapterManager = Platform.getAdapterManager();
230 				if (adapterManager.hasAdapter(editor,   IRunToLineTarget.class.getName())) {
231 					runToLineAction = (IRunToLineTarget) adapterManager.loadAdapter(editor,IRunToLineTarget.class.getName());
232 				}
233 			}
234 		}
235 		// if no adapter exists, use the Java adapter
236 		if (runToLineAction == null) {
237 		  runToLineAction = new RunToLineAdapter();
238 		}
239 		final IDebugEventSetListener listener = new IDebugEventSetListener() {
240 			/**
241 			 * @see IDebugEventSetListener#handleDebugEvents(DebugEvent[])
242 			 */
243 			@Override
244 			public void handleDebugEvents(DebugEvent[] events) {
245 				for (int i = 0; i < events.length; i++) {
246 					DebugEvent event = events[i];
247 					switch (event.getKind()) {
248 						case DebugEvent.SUSPEND :
249 							handleSuspendEvent(event);
250 							break;
251 						case DebugEvent.TERMINATE :
252 							handleTerminateEvent(event);
253 							break;
254 						default :
255 							break;
256 					}
257 				}
258 			}
259 			/**
260 			 * Listen for the completion of the "run to line." When the thread
261 			 * suspends at the correct location, perform a "step into selection"
262 			 * @param event the debug event
263 			 */
264 			private void handleSuspendEvent(DebugEvent event) {
265 				Object source = event.getSource();
266 				if (source instanceof IJavaThread) {
267 					try {
268 						final IJavaStackFrame frame= (IJavaStackFrame) ((IJavaThread) source).getTopStackFrame();
269 						if (isExpectedFrame(frame)) {
270 							DebugPlugin plugin = DebugPlugin.getDefault();
271 							plugin.removeDebugEventListener(this);
272 							plugin.asyncExec(new Runnable() {
273 								@Override
274 								public void run() {
275 									try {
276 										doStepIn(editor, frame, method);
277 									} catch (DebugException e) {
278 										showErrorMessage(editor, e.getStatus().getMessage());
279 									}
280 								}
281 							});
282 						}
283 					} catch (DebugException e) {
284 						return;
285 					}
286 				}
287 			}
288 			/**
289 			 * Returns whether the given frame is the frame that this action is expecting.
290 			 * This frame is expecting a stack frame for the suspension of the "run to line".
291 			 * @param frame the given stack frame or <code>null</code>
292 			 * @return whether the given stack frame is the expected frame
293 			 * @throws DebugException
294 			 */
295 			private boolean isExpectedFrame(IJavaStackFrame frame) throws DebugException {
296 				return frame != null &&
297 					line == frame.getLineNumber() &&
298 					frame.getReceivingTypeName().equals(typeName);
299 			}
300 			/**
301 			 * When the debug target we're listening for terminates, stop listening
302 			 * to debug events.
303 			 * @param event the debug event
304 			 */
305 			private void handleTerminateEvent(DebugEvent event) {
306 				Object source = event.getSource();
307 				if (thread.getDebugTarget() == source) {
308 					DebugPlugin.getDefault().removeDebugEventListener(this);
309 				}
310 			}
311 		};
312 		DebugPlugin.getDefault().addDebugEventListener(listener);
313 		try {
314 			runToLineAction.runToLine(editor, textSelection, thread);
315 		} catch (CoreException e) {
316 			DebugPlugin.getDefault().removeDebugEventListener(listener);
317 			showErrorMessage(editor, ActionMessages.StepIntoSelectionActionDelegate_4);
318 			JDIDebugUIPlugin.log(e.getStatus());
319 		}
320 	}
321 
322 	/**
323 	 * Steps into the given method in the given stack frame
324 	 *
325 	 * @param editor
326 	 * @param frame the frame in which the step should begin
327 	 * @param method the method to step into
328 	 * @throws DebugException
329 	 */
doStepIn(IEditorPart editor, IJavaStackFrame frame, IMethod method)330 	static void doStepIn(IEditorPart editor, IJavaStackFrame frame, IMethod method) throws DebugException {
331 		// ensure top stack frame
332 		IStackFrame tos = frame.getThread().getTopStackFrame();
333 		if (tos == null) {
334 			return;
335 		}
336 		if (!tos.equals(frame)) {
337 			showErrorMessage(editor, ActionMessages.StepIntoSelectionActionDelegate_Step_into_selection_only_available_in_top_stack_frame__3);
338 			return;
339 		}
340 		StepIntoSelectionHandler handler = new StepIntoSelectionHandler((IJavaThread)frame.getThread(), frame, method);
341 		handler.step();
342 	}
343 
344 	/**
345 	 * Displays an error message in the status area
346 	 *
347 	 * @param editor
348 	 * @param message
349 	 */
showErrorMessage(IEditorPart editor, String message)350 	static void showErrorMessage(IEditorPart editor, String message) {
351 		if (editor != null) {
352 			IEditorStatusLine statusLine = editor.getAdapter(IEditorStatusLine.class);
353 			if (statusLine != null) {
354 				statusLine.setMessage(true, message, null);
355 			}
356 		}
357 	}
358 
359 	/**
360 	 * Return the type containing the selected text, or <code>null</code> if the
361 	 * selection is not in a type.
362 	 */
getType(ITextSelection textSelection)363 	static IType getType(ITextSelection textSelection) {
364 		IMember member= ActionDelegateHelper.getDefault().getCurrentMember(textSelection);
365 		IType type= null;
366 		if (member instanceof IType) {
367 			type = (IType)member;
368 		} else if (member != null) {
369 			type= member.getDeclaringType();
370 		}
371 		return type;
372 	}
373 
374 	/**
375      * Strips inner class names and parameterized type information from the given type name.
376      *
377      * @param fullyQualifiedName
378      */
stripInnerNamesAndParameterType(String fullyQualifiedName)379     static String stripInnerNamesAndParameterType(String fullyQualifiedName) {
380         // ignore inner class qualification, as the compiler generated names and java model names can be different
381     	String sig = fullyQualifiedName;
382         int index = sig.indexOf('$');
383         if (index > 0) {
384            sig = sig.substring(0, index);
385         }
386         //also ignore erasure
387         index = sig.indexOf('<');
388         if (index > 0){
389         	sig = sig.substring(0, index);
390         }
391         return sig;
392     }
393 }
394