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);
}
}
}