From 944fdd97bab6fa4c0de898a50258388aef575ce8 Mon Sep 17 00:00:00 2001 From: Sougandh S Date: Sat, 14 Nov 2026 09:50:31 +0530 Subject: [PATCH] Show exit-only method breakpoint annotation at the closing brace Toggling a method exit breakpoint on the closing brace created the breakpoint, but its annotation stayed on the method declaration line. Place the marker on the closing brace for exit-only breakpoints, and on the method line when entry is also enabled. Toggling on the closing brace of an existing method breakpoint now switches its exit mode instead of deleting it. --- .../breakpoints/MethodBreakpointTests15.java | 109 +++++++++++++++++- .../debug/ui/BreakpointMarkerUpdater.java | 38 +++++- .../internal/debug/ui/BreakpointUtils.java | 50 +++++++- .../debug/ui/actions/EntryToggleAction.java | 7 +- .../debug/ui/actions/ExitToggleAction.java | 10 +- .../ui/actions/ToggleBreakpointAdapter.java | 15 +++ .../breakpoints/MethodBreakpointEditor.java | 4 +- 7 files changed, 225 insertions(+), 8 deletions(-) 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); } } }