diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/breakpoints/MethodBreakpointTests15.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/breakpoints/MethodBreakpointTests15.java index dc7300821c..54bc3babef 100644 --- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/breakpoints/MethodBreakpointTests15.java +++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/breakpoints/MethodBreakpointTests15.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2007, 2014 IBM Corporation and others. + * Copyright (c) 2007, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -19,10 +19,14 @@ import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.debug.core.model.IBreakpoint; +import org.eclipse.debug.ui.actions.ToggleBreakpointAction; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.debug.core.IJavaMethodBreakpoint; import org.eclipse.jdt.debug.core.IJavaThread; +import org.eclipse.jdt.debug.core.JDIDebugModel; +import org.eclipse.jdt.debug.tests.TestUtil; +import org.eclipse.jdt.debug.tests.VerticalRulerInfoStub; import org.eclipse.jdt.debug.tests.ui.AbstractDebugUiTests; import org.eclipse.ui.IEditorPart; @@ -72,6 +76,8 @@ private IJavaMethodBreakpoint createBreakpoint(int line) throws Exception { assertTrue("Missing file", resource instanceof IFile); IEditorPart editor = openEditor((IFile) resource); IBreakpoint breakpoint = toggleBreakpoint(editor, line); + // breakpointAdded fires before the toggle job finishes setting up the breakpoint + TestUtil.waitForJobs(getName(), 100, DEFAULT_TIMEOUT); assertTrue("Wrong breakpoint", breakpoint instanceof IJavaMethodBreakpoint); return (IJavaMethodBreakpoint) breakpoint; } @@ -130,4 +136,105 @@ public void testGenericArrayEntryBreakpoints() throws Exception { removeAllBreakpoints(); } } + + /** Line of the methodTypeParameter declaration in {@link #getTypeName()} */ + private static final int METHOD_LINE = 37; + + /** Line of the closing brace of methodTypeParameter in {@link #getTypeName()} */ + private static final int CLOSING_BRACE_LINE = 39; + + /** + * Toggling on the closing brace creates an exit only breakpoint, located on the closing brace line. + */ + public void testToggleOnClosingBraceCreatesExitOnlyBreakpoint() throws Exception { + try { + IJavaMethodBreakpoint breakpoint = createBreakpoint(CLOSING_BRACE_LINE); + assertFalse("Should not be an entry breakpoint", breakpoint.isEntry()); + assertTrue("Should be an exit breakpoint", breakpoint.isExit()); + assertEquals("Wrong line", CLOSING_BRACE_LINE, breakpoint.getLineNumber()); + } finally { + removeAllBreakpoints(); + } + } + + /** + * Toggling on the closing brace of an entry breakpoint adds exit to the same breakpoint, which stays on the method line. Toggling again + * removes exit. + */ + public void testToggleOnClosingBraceOfEntryBreakpoint() throws Exception { + try { + IJavaMethodBreakpoint breakpoint = createBreakpoint(METHOD_LINE); + assertTrue("Should be an entry breakpoint", breakpoint.isEntry()); + assertFalse("Should not be an exit breakpoint", breakpoint.isExit()); + IEditorPart editor = openEditor(); + + toggle(editor, CLOSING_BRACE_LINE); + assertSame("Breakpoint should be kept", breakpoint, getSingleBreakpoint()); + assertTrue("Should be an entry breakpoint", breakpoint.isEntry()); + assertTrue("Should be an exit breakpoint", breakpoint.isExit()); + assertEquals("Should stay on the method line", -1, breakpoint.getLineNumber()); + + toggle(editor, CLOSING_BRACE_LINE); + assertSame("Breakpoint should be kept", breakpoint, getSingleBreakpoint()); + assertTrue("Should be an entry breakpoint", breakpoint.isEntry()); + assertFalse("Should not be an exit breakpoint", breakpoint.isExit()); + } finally { + removeAllBreakpoints(); + } + } + + /** + * Toggling on the method line of an exit only breakpoint adds entry to the same breakpoint and moves it to the method line. + */ + public void testToggleOnMethodLineOfExitOnlyBreakpoint() throws Exception { + try { + IJavaMethodBreakpoint breakpoint = createBreakpoint(CLOSING_BRACE_LINE); + assertFalse("Should not be an entry breakpoint", breakpoint.isEntry()); + assertTrue("Should be an exit breakpoint", breakpoint.isExit()); + + toggle(openEditor(), METHOD_LINE); + assertSame("Breakpoint should be kept", breakpoint, getSingleBreakpoint()); + assertTrue("Should be an entry breakpoint", breakpoint.isEntry()); + assertTrue("Should be an exit breakpoint", breakpoint.isExit()); + assertEquals("Should be on the method line", -1, breakpoint.getLineNumber()); + } finally { + removeAllBreakpoints(); + } + } + + /** + * Toggling on the closing brace of an exit only breakpoint removes it. + */ + public void testToggleOnClosingBraceOfExitOnlyBreakpoint() throws Exception { + try { + createBreakpoint(CLOSING_BRACE_LINE); + + toggle(openEditor(), CLOSING_BRACE_LINE); + assertEquals("Breakpoint should be removed", 0, getBreakpointManager().getBreakpoints(JDIDebugModel.getPluginIdentifier()).length); + } finally { + removeAllBreakpoints(); + } + } + + private IEditorPart openEditor() throws Exception { + IType type = get15Project().findType(getTypeName()); + assertNotNull("Missing file", type); + IResource resource = type.getResource(); + assertTrue("Missing file", resource instanceof IFile); + return openEditor((IFile) resource); + } + + /** + * Toggles at the given line (1 based) and waits for the asynchronous toggle job. + */ + private void toggle(IEditorPart editor, int line) throws Exception { + sync(() -> new ToggleBreakpointAction(editor, null, new VerticalRulerInfoStub(line - 1)).run()); + TestUtil.waitForJobs(getName(), 100, DEFAULT_TIMEOUT); + } + + private IBreakpoint getSingleBreakpoint() { + IBreakpoint[] breakpoints = getBreakpointManager().getBreakpoints(JDIDebugModel.getPluginIdentifier()); + assertEquals("Wrong number of breakpoints", 1, breakpoints.length); + return breakpoints[0]; + } } \ No newline at end of file diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/BreakpointMarkerUpdater.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/BreakpointMarkerUpdater.java index ad267f2247..823dba1beb 100644 --- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/BreakpointMarkerUpdater.java +++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/BreakpointMarkerUpdater.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2006, 2021 IBM Corporation and others. + * Copyright (c) 2006, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -22,7 +22,11 @@ import org.eclipse.debug.core.model.IBreakpoint; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Block; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.NodeFinder; import org.eclipse.jdt.core.manipulation.SharedASTProviderCore; import org.eclipse.jdt.debug.core.IJavaLineBreakpoint; import org.eclipse.jdt.debug.core.IJavaMethodBreakpoint; @@ -112,6 +116,18 @@ public boolean updateMarker(IMarker marker, IDocument document, Position positio return false; } try { + if (breakpoint instanceof IJavaMethodBreakpoint mBreakpoint && !mBreakpoint.isLambdaBreakpoint() && mBreakpoint.isExit() && !mBreakpoint.isEntry() + && MarkerUtilities.getLineNumber(marker) != -1) { + // exit-only method breakpoint is located on the closing brace of its method: keep it only while that is still the case + if (!isClosingBraceOfMethod(unit, position.getOffset(), mBreakpoint.getMethodName())) { + return false; + } + int exitLine = document.getLineOfOffset(position.getOffset()) + 1; + if (MarkerUtilities.getLineNumber(marker) != exitLine) { + MarkerUtilities.setLineNumber(marker, exitLine); + } + return true; + } ValidBreakpointLocationLocator loc; if (breakpoint instanceof IJavaMethodBreakpoint && ((IJavaMethodBreakpoint) breakpoint).isEntry()) { IMarker m = breakpoint.getMarker(); @@ -162,6 +178,26 @@ public boolean updateMarker(IMarker marker, IDocument document, Position positio return false; } + /** + * Returns whether the given offset is the closing brace of the body of the method with the given name. + * + * @param unit the AST of the current source + * @param offset the offset to check + * @param methodName the method name of the breakpoint, <init> for constructors + * @return true if the offset is the closing brace of the method, false otherwise + */ + private boolean isClosingBraceOfMethod(CompilationUnit unit, int offset, String methodName) { + ASTNode node = NodeFinder.perform(unit, offset, 1); + if (!(node instanceof Block block) || !(block.getParent() instanceof MethodDeclaration method)) { + return false; + } + if (block.getStartPosition() + block.getLength() - 1 != offset) { + return false; + } + String name = method.isConstructor() ? "" : method.getName().getIdentifier(); //$NON-NLS-1$ + return name.equals(methodName); + } + /** * Updates the charstart and charend ranges if necessary for the given line. * Returns immediately if the line is not valid (< 0 or greater than the total line number count) diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/BreakpointUtils.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/BreakpointUtils.java index 82e1ce50a9..757731a728 100644 --- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/BreakpointUtils.java +++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/BreakpointUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2012 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -28,6 +28,7 @@ import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IMember; import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; @@ -36,6 +37,7 @@ import org.eclipse.jdt.debug.core.IJavaMethodBreakpoint; import org.eclipse.jdt.debug.core.IJavaWatchpoint; import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; @@ -265,6 +267,52 @@ public static void addRunToLineAttributes(Map attributes) { attributes.put(RUN_TO_LINE, Boolean.TRUE); } + /** + * Positions the given method breakpoint's marker: at the closing brace of the method if the breakpoint is an exit-only breakpoint, otherwise at + * the method name (the default location). + * + * @param breakpoint the method breakpoint + * @throws CoreException if accessing the breakpoint or the Java model fails + */ + public static void updateMethodBreakpointLocation(IJavaMethodBreakpoint breakpoint) throws CoreException { + if (breakpoint.isLambdaBreakpoint()) { + return; + } + IMethod method = getMethod(breakpoint); + if (method == null || !method.exists() || method.getTypeRoot() == null) { + return; + } + ISourceRange nameRange = method.getNameRange(); + ISourceRange methodRange = method.getSourceRange(); + if (nameRange == null || methodRange == null || nameRange.getOffset() < 0 || methodRange.getOffset() < 0) { + return; + } + int start = nameRange.getOffset(); + int end = start + nameRange.getLength(); + int line = -1; + if (breakpoint.isExit() && !breakpoint.isEntry()) { + String source = method.getCompilationUnit().getSource(); + int closingBrace = methodRange.getOffset() + methodRange.getLength() - 1; + if (source == null || closingBrace < 0 || closingBrace >= source.length()) { + return; + } + try { + line = new Document(source).getLineOfOffset(closingBrace) + 1; + } catch (BadLocationException e) { + return; + } + start = closingBrace; + end = closingBrace + 1; + } + IMarker marker = breakpoint.getMarker(); + if (marker.getAttribute(IMarker.CHAR_START, -1) == start && marker.getAttribute(IMarker.CHAR_END, -1) == end + && marker.getAttribute(IMarker.LINE_NUMBER, -1) == line) { + return; + } + marker.setAttributes(new String[] { IMarker.CHAR_START, IMarker.CHAR_END, IMarker.LINE_NUMBER }, + new Object[] { Integer.valueOf(start), Integer.valueOf(end), Integer.valueOf(line) }); + } + /** * Returns the method associated with the method entry * breakpoint. diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/EntryToggleAction.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/EntryToggleAction.java index 2995da17e5..e58d51a7bb 100644 --- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/EntryToggleAction.java +++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/EntryToggleAction.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2025 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -19,6 +19,7 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.jdt.debug.core.IJavaBreakpoint; import org.eclipse.jdt.debug.core.IJavaMethodBreakpoint; +import org.eclipse.jdt.internal.debug.ui.BreakpointUtils; import org.eclipse.jface.viewers.IStructuredSelection; public class EntryToggleAction extends BreakpointToggleAction { @@ -36,7 +37,9 @@ protected boolean getToggleState(IJavaBreakpoint breakpoint) throws CoreExceptio */ @Override public void doAction(IJavaBreakpoint breakpoint) throws CoreException { - ((IJavaMethodBreakpoint)breakpoint).setEntry(!((IJavaMethodBreakpoint)breakpoint).isEntry()); + IJavaMethodBreakpoint methodBreakpoint = (IJavaMethodBreakpoint) breakpoint; + methodBreakpoint.setEntry(!methodBreakpoint.isEntry()); + BreakpointUtils.updateMethodBreakpointLocation(methodBreakpoint); } /** diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/ExitToggleAction.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/ExitToggleAction.java index 2502662185..7fd3afb476 100644 --- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/ExitToggleAction.java +++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/ExitToggleAction.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2025 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -19,6 +19,7 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.jdt.debug.core.IJavaBreakpoint; import org.eclipse.jdt.debug.core.IJavaMethodBreakpoint; +import org.eclipse.jdt.internal.debug.ui.BreakpointUtils; import org.eclipse.jface.viewers.IStructuredSelection; public class ExitToggleAction extends BreakpointToggleAction { @@ -36,7 +37,12 @@ protected boolean getToggleState(IJavaBreakpoint breakpoint) throws CoreExceptio */ @Override public void doAction(IJavaBreakpoint breakpoint) throws CoreException { - ((IJavaMethodBreakpoint)breakpoint).setExit(!((IJavaMethodBreakpoint)breakpoint).isExit()); + if (!(breakpoint instanceof IJavaMethodBreakpoint)) { + return; + } + IJavaMethodBreakpoint methodBreakpoint = (IJavaMethodBreakpoint) breakpoint; + methodBreakpoint.setExit(!methodBreakpoint.isExit()); + BreakpointUtils.updateMethodBreakpointLocation(methodBreakpoint); } /** diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/ToggleBreakpointAdapter.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/ToggleBreakpointAdapter.java index e7836f57dd..530cee3861 100644 --- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/ToggleBreakpointAdapter.java +++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/actions/ToggleBreakpointAdapter.java @@ -420,6 +420,20 @@ private static void doToggleMethodBreakpoint(IMethod member, String lambdaMethod if (BreakpointToggleUtils.isToggleTracepoint()) { deleteTracepoint(breakpoint, part, monitor); BreakpointToggleUtils.setUnsetTracepoint(false); + } else if (ValidBreakpointLocationLocator.LOCATION_METHOD_CLOSE && breakpoint instanceof IJavaMethodBreakpoint existing && !existing.isLambdaBreakpoint()) { + // toggled at the method's closing brace: switch the exit mode of the existing breakpoint instead of deleting it + ValidBreakpointLocationLocator.LOCATION_METHOD_CLOSE = false; + if (existing.isExit() && !existing.isEntry()) { + deleteBreakpoint(breakpoint, part, monitor); + } else { + existing.setExit(!existing.isExit()); + BreakpointUtils.updateMethodBreakpointLocation(existing); + } + } else if (!ValidBreakpointLocationLocator.LOCATION_METHOD_CLOSE && finalSelection instanceof ITextSelection && breakpoint instanceof IJavaMethodBreakpoint existing + && !existing.isLambdaBreakpoint() && existing.isExit() && !existing.isEntry()) { + // toggled at the method line of an exit-only breakpoint: add entry instead of deleting it + existing.setEntry(true); + BreakpointUtils.updateMethodBreakpointLocation(existing); } else { if (ValidBreakpointLocationLocator.LOCATION_METHOD_CLOSE) { ValidBreakpointLocationLocator.LOCATION_METHOD_CLOSE = false; @@ -477,6 +491,7 @@ private static void doToggleMethodBreakpoint(IMethod member, String lambdaMethod if (ValidBreakpointLocationLocator.LOCATION_METHOD_CLOSE) { methodBreakpoint.setEntry(false); methodBreakpoint.setExit(true); + BreakpointUtils.updateMethodBreakpointLocation(methodBreakpoint); ValidBreakpointLocationLocator.LOCATION_METHOD_CLOSE = false; } diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/breakpoints/MethodBreakpointEditor.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/breakpoints/MethodBreakpointEditor.java index f64717b414..34501a627a 100644 --- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/breakpoints/MethodBreakpointEditor.java +++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/breakpoints/MethodBreakpointEditor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2009, 2011 IBM Corporation and others. + * Copyright (c) 2009, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -17,6 +17,7 @@ import org.eclipse.debug.internal.ui.SWTFactory; import org.eclipse.jdt.debug.core.IJavaBreakpoint; import org.eclipse.jdt.debug.core.IJavaMethodBreakpoint; +import org.eclipse.jdt.internal.debug.ui.BreakpointUtils; import org.eclipse.jdt.internal.debug.ui.propertypages.PropertyPageMessages; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; @@ -84,6 +85,7 @@ public void doSave() throws CoreException { if (breakpoint instanceof IJavaMethodBreakpoint watchpoint) { watchpoint.setEntry(fEntry.getSelection()); watchpoint.setExit(fExit.getSelection()); + BreakpointUtils.updateMethodBreakpointLocation(watchpoint); } } }