From 978f6ce94ff3d24932fb1443cf04813f6b4d36cb Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Mon, 19 Oct 2026 12:04:13 +0200 Subject: [PATCH 01/13] Log point easily dumping local variables --- .../debugger/jpda/ui/BreakpointOutput.java | 57 +++++++++++++--- .../jpda/ui/actions/AddLogpointAction.java | 67 +++++++++++++++++++ 2 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java index 002797a361e7..d327de2fc725 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java @@ -75,6 +75,7 @@ public class BreakpointOutput extends LazyActionsManagerListener ("\\\\"); private static final String threadNamePattern = "{threadName}"; private static final String classNamePattern = "{className}"; + private static final String localsPattern = "{locals}"; private static final Pattern methodNamePattern = Pattern.compile ("\\{methodName\\}"); private static final Pattern lineNumberPattern = Pattern.compile @@ -375,6 +376,11 @@ private void printOutEvent(DebuggerConsoleIO io, String format, JPDABreakpointEv printText = selectCondition(printText, threadStartedCondition, false); } } + + int printLocals = printText.indexOf(localsPattern); + if (printLocals != -1) { + printText = printText.substring(0, printLocals) + printText.substring(printLocals + localsPattern.length()); + } // 5) resolve all expressions {=expression} printExpressions(event, printText, (msg, action) -> { @@ -382,6 +388,33 @@ private void printOutEvent(DebuggerConsoleIO io, String format, JPDABreakpointEv }, (msg) -> { io.println(msg, null); }); + + if (printLocals != -1) { + CallStackFrame csf = findTopFrame(event); + if (csf != null) { + try { + for (var refValue : csf.getLocalVariables()) { + io.printWithAction(" ", null, false); + io.printWithAction(refValue.getName(), null, false); + io.printWithAction(" = ", null, false); + if (refValue instanceof ObjectVariable obj && !isNull(obj)) { + var sessionRef = new WeakReference<>(session); + io.printWithAction(obj.getValue(), () -> { + var s = sessionRef.get(); + var ok = s != null && ObjectInfoProvider.showObjectInfo(s, obj); + if (!ok) { + io.println("Cannot display details of " + obj.getValue(), null); + } + }, true); + } else { + io.println(refValue.getValue(), null); + } + } + } catch (AbsentInformationException ex) { + // no locals info + } + } + } } /* Processing all expressions. Expressions take form of {=expression}. @@ -415,16 +448,7 @@ final void printExpressions( String beforeExpression = printText.substring(0, m.start()); print.accept(beforeExpression, null); - CallStackFrame csf = null; - JPDAThread t = event.getThread(); - if (t != null && !event.isThreadDeath()) { - try { - CallStackFrame[] topFramePtr = t.getCallStack(0, 1); - if (topFramePtr.length > 0) { - csf = topFramePtr[0]; - } - } catch (AbsentInformationException aiex) {} - } + CallStackFrame csf = findTopFrame(event); Variable refValue = theDebugger.evaluate(expression, csf); if (refValue instanceof ObjectVariable obj && !isNull(obj)) { var sessionRef = new WeakReference<>(session); @@ -468,6 +492,19 @@ final void printExpressions( } } + private CallStackFrame findTopFrame(JPDABreakpointEvent event) { + if (event.getThread() != null && !event.isThreadDeath()) { + try { + CallStackFrame[] topFramePtr = event.getThread().getCallStack(0, 1); + if (topFramePtr.length > 0) { + return topFramePtr[0]; + } + } catch (AbsentInformationException aiex) { + } + } + return null; + } + private static boolean isNull(ObjectVariable obj) { return obj.getUniqueID() == 0; } diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java new file mode 100644 index 000000000000..538199523754 --- /dev/null +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.debugger.jpda.ui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import org.netbeans.api.debugger.DebuggerManager; +import org.netbeans.api.debugger.jpda.JPDABreakpoint; +import org.netbeans.api.debugger.jpda.LineBreakpoint; +import org.netbeans.modules.debugger.jpda.ui.EditorContextBridge; +import org.openide.cookies.EditorCookie; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.util.NbBundle.Messages; + +@ActionID( + category = "Debug", + id = "org.netbeans.modules.debugger.jpda.ui.actions.AddLogpointAction" +) +@ActionRegistration( + displayName = "#CTL_AddLogpointAction" +) +@ActionReference(path = "Editors/text/x-java/Popup", position = 1900) +@Messages({ + "CTL_AddLogpointAction=Add &Logpoint", + "# {0} - clazz name", + "# {1} - method name", + "# {2} - line number", + "# {3} - suffix", + "MSG_LogpointAt=Logpoint at {0}.{1}:{2} {3}" +}) +public final class AddLogpointAction implements ActionListener { + private final EditorCookie context; + + public AddLogpointAction(EditorCookie context) { + this.context = context; + } + + @Override + public void actionPerformed(ActionEvent ev) { + String where = EditorContextBridge.getContext().getCurrentURL(); + int at = EditorContextBridge.getContext().getCurrentLineNumber(); + + LineBreakpoint lb = LineBreakpoint.create(where, at); + lb.setSuspend(JPDABreakpoint.SUSPEND_NONE); + lb.setPrintText(Bundle.MSG_LogpointAt("{className}", "{methodName}", at, "{locals}")); + + DebuggerManager.getDebuggerManager().addBreakpoint(lb); + } +} From 8685bd705815b52ed06d979cc857573fab7738ee Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Mon, 19 Oct 2026 19:52:25 +0200 Subject: [PATCH 02/13] Using {lineNumber} placeholder --- .../modules/debugger/jpda/ui/actions/AddLogpointAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java index 538199523754..822bd79b0b65 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java @@ -60,7 +60,7 @@ public void actionPerformed(ActionEvent ev) { LineBreakpoint lb = LineBreakpoint.create(where, at); lb.setSuspend(JPDABreakpoint.SUSPEND_NONE); - lb.setPrintText(Bundle.MSG_LogpointAt("{className}", "{methodName}", at, "{locals}")); + lb.setPrintText(Bundle.MSG_LogpointAt("{className}", "{methodName}", "{lineNumber}", "{locals}")); DebuggerManager.getDebuggerManager().addBreakpoint(lb); } From 8ad2c53d1b64d2f5b3ce438172a7ae51e4e836a8 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Mon, 19 Oct 2026 20:17:27 +0200 Subject: [PATCH 03/13] Only enable the logpoint action on Java sources --- .../jpda/ui/actions/AddLogpointAction.java | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java index 822bd79b0b65..9289e66fbe0d 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/actions/AddLogpointAction.java @@ -20,14 +20,18 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import javax.swing.AbstractAction; +import javax.swing.Action; import org.netbeans.api.debugger.DebuggerManager; import org.netbeans.api.debugger.jpda.JPDABreakpoint; import org.netbeans.api.debugger.jpda.LineBreakpoint; import org.netbeans.modules.debugger.jpda.ui.EditorContextBridge; -import org.openide.cookies.EditorCookie; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; +import org.openide.awt.ActionReferences; import org.openide.awt.ActionRegistration; +import org.openide.awt.ActionState; +import org.openide.loaders.DataObject; import org.openide.util.NbBundle.Messages; @ActionID( @@ -35,9 +39,13 @@ id = "org.netbeans.modules.debugger.jpda.ui.actions.AddLogpointAction" ) @ActionRegistration( - displayName = "#CTL_AddLogpointAction" + displayName = "#CTL_AddLogpointAction", + enabledOn = @ActionState( property = "hasJavaSource", type = Action.class ) ) -@ActionReference(path = "Editors/text/x-java/Popup", position = 1900) +@ActionReferences({ + @ActionReference(path = "Editors/text/x-java/Popup", position = 1900), + @ActionReference(path = "Editors/AnnotationTypes/BreakpointActionsWithToggleNew", position = 400) +}) @Messages({ "CTL_AddLogpointAction=Add &Logpoint", "# {0} - clazz name", @@ -46,13 +54,23 @@ "# {3} - suffix", "MSG_LogpointAt=Logpoint at {0}.{1}:{2} {3}" }) -public final class AddLogpointAction implements ActionListener { - private final EditorCookie context; +public final class AddLogpointAction extends AbstractAction { + private final DataObject context; - public AddLogpointAction(EditorCookie context) { + public AddLogpointAction(DataObject context) { this.context = context; } + @Override + public Object getValue(String key) { + if ("hasJavaSource".equals(key)) { + String mime = context.getPrimaryFile().getMIMEType("text/x-java"); + return "text/x-java".equals(mime); + } + return super.getValue(key); + } + + @Override public void actionPerformed(ActionEvent ev) { String where = EditorContextBridge.getContext().getCurrentURL(); From 64af419b5c4e644d8649d122320298dc2dddcae5 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Mon, 19 Oct 2026 20:17:42 +0200 Subject: [PATCH 04/13] Removing unnecessary imports --- .../modules/debugger/jpda/ui/EditorContextBridge.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/EditorContextBridge.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/EditorContextBridge.java index a28784be4440..530c3451402f 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/EditorContextBridge.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/EditorContextBridge.java @@ -23,19 +23,12 @@ import java.io.File; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import javax.swing.JEditorPane; -import org.netbeans.api.debugger.Breakpoint.VALIDITY; import org.netbeans.api.debugger.DebuggerManager; -import org.netbeans.api.debugger.jpda.FieldBreakpoint; -import org.netbeans.api.debugger.jpda.JPDABreakpoint; import org.netbeans.api.debugger.jpda.LineBreakpoint; -import org.netbeans.api.debugger.jpda.MethodBreakpoint; import org.netbeans.api.debugger.jpda.CallStackFrame; -import org.netbeans.api.debugger.jpda.JPDADebugger; import org.netbeans.api.debugger.jpda.JPDAThread; import org.netbeans.spi.debugger.jpda.EditorContext; import org.netbeans.spi.debugger.jpda.SourcePathProvider; @@ -44,7 +37,6 @@ import org.openide.ErrorManager; import org.openide.filesystems.FileObject; import org.openide.util.Exceptions; -import org.openide.util.Utilities; /** From 15eb8c41921441dde0879dd7936d1a45971cca2f Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Mon, 19 Oct 2026 20:44:44 +0200 Subject: [PATCH 05/13] Unit testing printLocals --- .../debugger/jpda/ui/BreakpointOutput.java | 84 +++++++++--------- .../jpda/ui/views/BreakpointOutputWriter.java | 26 ++++++ .../jpda/ui/BreakpointOutputTest.java | 85 ++++++++++++++++--- 3 files changed, 140 insertions(+), 55 deletions(-) create mode 100644 java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/views/BreakpointOutputWriter.java diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java index d327de2fc725..a7caa9fc08a0 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java @@ -25,8 +25,6 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.netbeans.api.debugger.ActionsManagerListener; @@ -53,6 +51,7 @@ import org.netbeans.modules.debugger.jpda.JPDADebuggerImpl; import org.netbeans.modules.debugger.jpda.ui.models.BreakpointsActionsProvider; import org.netbeans.modules.debugger.jpda.ui.models.BreakpointsNodeModel; +import org.netbeans.modules.debugger.jpda.ui.views.BreakpointOutputWriter; import org.netbeans.spi.debugger.ContextProvider; import org.netbeans.spi.debugger.jpda.ObjectInfoProvider; import org.netbeans.spi.viewmodel.NodeModel; @@ -383,37 +382,10 @@ private void printOutEvent(DebuggerConsoleIO io, String format, JPDABreakpointEv } // 5) resolve all expressions {=expression} - printExpressions(event, printText, (msg, action) -> { - io.printWithAction(msg, action, false); - }, (msg) -> { - io.println(msg, null); - }); + printExpressions(event, printText, io::printWithAction); if (printLocals != -1) { - CallStackFrame csf = findTopFrame(event); - if (csf != null) { - try { - for (var refValue : csf.getLocalVariables()) { - io.printWithAction(" ", null, false); - io.printWithAction(refValue.getName(), null, false); - io.printWithAction(" = ", null, false); - if (refValue instanceof ObjectVariable obj && !isNull(obj)) { - var sessionRef = new WeakReference<>(session); - io.printWithAction(obj.getValue(), () -> { - var s = sessionRef.get(); - var ok = s != null && ObjectInfoProvider.showObjectInfo(s, obj); - if (!ok) { - io.println("Cannot display details of " + obj.getValue(), null); - } - }, true); - } else { - io.println(refValue.getValue(), null); - } - } - } catch (AbsentInformationException ex) { - // no locals info - } - } + printLocals(event, io::printWithAction); } } @@ -423,8 +395,7 @@ private void printOutEvent(DebuggerConsoleIO io, String format, JPDABreakpointEv final void printExpressions( JPDABreakpointEvent event, String message, - BiConsumer print, - Consumer println + BreakpointOutputWriter io ) { String printText = message; Session session = event.getDebugger().getSession(); @@ -446,22 +417,22 @@ final void printExpressions( } String beforeExpression = printText.substring(0, m.start()); - print.accept(beforeExpression, null); + io.print(beforeExpression, null, false); CallStackFrame csf = findTopFrame(event); Variable refValue = theDebugger.evaluate(expression, csf); if (refValue instanceof ObjectVariable obj && !isNull(obj)) { var sessionRef = new WeakReference<>(session); - print.accept(obj.getValue(), () -> { + io.print(obj.getValue(), () -> { var s = sessionRef.get(); var ok = s != null && ObjectInfoProvider.showObjectInfo(s, obj); if (!ok) { - println.accept("Cannot display details of " + obj.getValue()); + io.print("Cannot display details of " + obj.getValue(), null, true); } - }); + }, false); } else { String textualValue = refValue == null ? "null" : refValue.getValue(); - print.accept(textualValue, null); + io.print(textualValue, null, false); } } catch (Exception e) { // expression is invalid or cannot be evaluated @@ -472,11 +443,10 @@ final void printExpressions( BreakpointsActionsProvider.customize(bp); }; dumpErrors.add(() -> { - print.accept(" ", null); - print.accept(msg, configure); - println.accept(""); + io.print(" ", null, false); + io.print(msg, configure, true); }); - print.accept("{=" + expression + "}", configure); + io.print("{=" + expression + "}", configure, false); } // process the rest of the text printText = printText.substring(m.end()); @@ -486,12 +456,40 @@ final void printExpressions( printText = printText + "\n***\n"+ thr.getLocalizedMessage()+"\n***\n"; } // remaining of the text with a new line - println.accept(printText); + io.print(printText, null, true); for (var errors : dumpErrors) { errors.run(); } } + final void printLocals(JPDABreakpointEvent event, BreakpointOutputWriter io) { + CallStackFrame csf = findTopFrame(event); + if (csf != null) { + try { + for (var refValue : csf.getLocalVariables()) { + io.print(" ", null, false); + io.print(refValue.getName(), null, false); + io.print(" = ", null, false); + if (refValue instanceof ObjectVariable obj && !isNull(obj)) { + var sessionRef = new WeakReference<>(event.getDebugger().getSession()); + io.print(obj.getValue(), () -> { + var s = sessionRef.get(); + var ok = s != null && ObjectInfoProvider.showObjectInfo(s, obj); + if (!ok) { + io.print("Cannot display details of " + obj.getValue(), null, true); + } + }, true); + } else { + io.print(refValue.getValue(), null, true); + } + } + } catch (AbsentInformationException ex) { + // no locals info + } + } + } + + private CallStackFrame findTopFrame(JPDABreakpointEvent event) { if (event.getThread() != null && !event.isThreadDeath()) { try { diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/views/BreakpointOutputWriter.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/views/BreakpointOutputWriter.java new file mode 100644 index 000000000000..abab0d01e29f --- /dev/null +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/views/BreakpointOutputWriter.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.debugger.jpda.ui.views; + +/** + * Abstraction of console output writer for easier mocking while testing. + */ +public interface BreakpointOutputWriter { + void print(String txt, Runnable action, boolean newLine); +} diff --git a/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutputTest.java b/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutputTest.java index 693fea79b38a..9a36d1ef43c6 100644 --- a/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutputTest.java +++ b/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutputTest.java @@ -24,8 +24,6 @@ import java.io.StringWriter; import java.lang.reflect.Method; import java.net.URI; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import junit.framework.Test; import org.netbeans.api.debugger.DebuggerManager; import org.netbeans.api.debugger.Session; @@ -35,6 +33,7 @@ import org.netbeans.api.debugger.jpda.event.JPDABreakpointEvent; import org.netbeans.api.debugger.jpda.event.JPDABreakpointListener; import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.debugger.jpda.ui.views.BreakpointOutputWriter; import org.openide.util.Utilities; /** Testing {@link BreakpointOutput#printExpressions} @@ -83,7 +82,7 @@ public void testBreakpointOutputLinks() throws Throwable { assertOnBreakpoint((event) -> { writer.append("on ").append(event.getThread().getName()).append("\n"); try { - printExpressions(out, event, format, (msg, action) -> { + printExpressions(out, event, format, (msg, action, newLine) -> { if (action != null) { writer.append(""); } - }, (msg) -> { - writer.append(msg).append("\n"); + if (newLine) { + writer.append("\n"); + } }); } catch (ReflectiveOperationException ex) { ex.printStackTrace(writer); @@ -108,6 +108,49 @@ public void testBreakpointOutputLinks() throws Throwable { """, collected.toString()); } + public void testLocalVariablesOutput() throws Throwable { + final Session session = support.getDebugger().getSession(); + + // + // now the real test and collect the printed messages + // + + var collected = new StringWriter(); + var writer = new PrintWriter(collected); + BreakpointOutput out = new BreakpointOutput(session); + assertOnBreakpoint((event) -> { + writer.append("on ").append(event.getThread().getName()).append("\n"); + try { + printLocals(out, event, (msg, action, newLine) -> { + if (action != null) { + writer.append(""); + } + if (newLine) { + writer.append("\n"); + } + }); + } catch (ReflectiveOperationException ex) { + ex.printStackTrace(writer); + } + }); + + // + // verify result + // + assertMatches(""" + on main + args = + i = 42 + when = + arr = + obj = + """, collected.toString()); + } + public void testArithmeticException() throws Exception { // // now the real test and collect the printed messages @@ -120,7 +163,7 @@ public void testArithmeticException() throws Exception { assertOnBreakpoint((event) -> { writer.append("on ").append(event.getThread().getName()).append("\n"); try { - printExpressions(out, event, format, (msg, action) -> { + printExpressions(out, event, format, (msg, action, newLine) -> { if (action != null) { writer.append(""); } - }, (msg) -> { - writer.append(msg).append("\n"); + if (newLine) { + writer.append("\n"); + } }); } catch (ReflectiveOperationException ex) { ex.printStackTrace(writer); @@ -192,15 +236,32 @@ private void printExpressions( BreakpointOutput out, JPDABreakpointEvent event, String message, - BiConsumer print, - Consumer println + BreakpointOutputWriter writer ) throws ReflectiveOperationException { Method m = out.getClass().getDeclaredMethod( "printExpressions", - JPDABreakpointEvent.class, String.class, BiConsumer.class, Consumer.class + JPDABreakpointEvent.class, String.class, BreakpointOutputWriter.class + ); + m.setAccessible(true); + m.invoke(out, event, message, writer); + } + + /** + * Calls {@link BreakpointOutput#printLocals}. + * Have to use reflection even the class and its tests are in the + * same package. They are loaded by different classloaders. + */ + private void printLocals( + BreakpointOutput out, + JPDABreakpointEvent event, + BreakpointOutputWriter writer + ) throws ReflectiveOperationException { + Method m = out.getClass().getDeclaredMethod( + "printLocals", + JPDABreakpointEvent.class, BreakpointOutputWriter.class ); m.setAccessible(true); - m.invoke(out, event, message, print, println); + m.invoke(out, event, writer); } private void assertMatches(String expected, String actual) { From e644433cf771ee446d136b1976b22ba47a78932e Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Fri, 23 Oct 2026 09:17:46 +0200 Subject: [PATCH 06/13] Valid and failing (right now) logpoint review test --- .../debugger/jpda/ui/LogpointReviewTest.java | 117 ++++++++++++++++++ .../unit/src/test/LogpointReviewMain.java | 32 +++++ 2 files changed, 149 insertions(+) create mode 100644 java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/LogpointReviewTest.java create mode 100644 java/debugger.jpda.ui/test/unit/src/test/LogpointReviewMain.java diff --git a/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/LogpointReviewTest.java b/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/LogpointReviewTest.java new file mode 100644 index 000000000000..a24764167579 --- /dev/null +++ b/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/LogpointReviewTest.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.debugger.jpda.ui; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import junit.framework.Test; +import org.netbeans.api.debugger.DebuggerManager; +import org.netbeans.api.debugger.jpda.ExceptionBreakpoint; +import org.netbeans.api.debugger.jpda.JPDADebugger; +import org.netbeans.api.debugger.jpda.JPDASupport; +import org.netbeans.api.debugger.jpda.ObjectVariable; +import org.netbeans.junit.NbTestCase; +import org.openide.util.Utilities; + +public class LogpointReviewTest extends NbTestCase { + + public LogpointReviewTest(String name) { + super(name); + } + + public static Test suite() { + return JPDASupport.createTestSuite(LogpointReviewTest.class); + } + + public void testEarlierLocalHyperlinkKeepsObjectIdentity() throws Exception, Throwable { + var bp = ExceptionBreakpoint.create("java.lang.IllegalStateException", ExceptionBreakpoint.TYPE_EXCEPTION_CAUGHT_UNCAUGHT); + bp.setPrintText(""); + DebuggerManager.getDebuggerManager().addBreakpoint(bp); + System.setProperty("test.dir.src", new File("test/unit/src").getAbsolutePath()); + AtomicReference error = new AtomicReference<>(); + ObjectVariable[] captured = {null}; + long[] originalId = {0}; + int[] hits = {0}; + bp.addJPDABreakpointListener(event -> { + try { + var output = new BreakpointOutput(event.getDebugger().getSession()); + Object io = event.getDebugger().getClass().getMethod("getConsoleIO").invoke(event.getDebugger()); + Object manager = field(field(io, "output"), "ioManager"); + List buffer = (List) field(manager, "buffer"); + synchronized (buffer) { + buffer.clear(); + output.substituteAndPrintText("REVIEW {locals}", event); + hits[0]++; + if (hits[0] == 1) { + boolean item = false; + for (Object text : buffer) { + if ("item".equals(field(text, "text"))) { + item = true; + } + Object action = field(text, "action"); + if (item && action != null) { + for (Field f : action.getClass().getDeclaredFields()) { + f.setAccessible(true); + if (f.get(action) instanceof ObjectVariable obj) { + captured[0] = obj; + } + } + assertNotNull("Hyperlink captures object", captured[0]); + originalId[0] = captured[0].getUniqueID(); + break; + } + } + } else { + assertNotNull("First item hyperlink captured", captured[0]); + System.out.println("REVIEW hyperlink original=" + originalId[0] + " current=" + captured[0].getUniqueID()); + assertEquals("Earlier hyperlink should still target the first object", originalId[0], captured[0].getUniqueID()); + } + buffer.clear(); + } + } catch (Throwable ex) { + error.set(ex); + } finally { + event.resume(); + } + }); + var support = JPDASupport.attach(new String[0], "test.LogpointReviewMain", new String[0], new File[] { + Utilities.toFile(test.LogpointReviewMain.class.getProtectionDomain().getCodeSource().getLocation().toURI()) + }); + for (;;) { + support.waitState(JPDADebugger.STATE_STOPPED); + if (support.getDebugger().getState() == JPDADebugger.STATE_DISCONNECTED) { + break; + } + support.doContinue(); + } + DebuggerManager.getDebuggerManager().removeBreakpoint(bp); + if (error.get() instanceof Throwable t) { + throw t; + } + assertEquals("Two loop iterations", 2, hits[0]); + } + + private static Object field(Object target, String name) throws Exception { + Field f = target.getClass().getDeclaredField(name); + f.setAccessible(true); + return f.get(target); + } +} diff --git a/java/debugger.jpda.ui/test/unit/src/test/LogpointReviewMain.java b/java/debugger.jpda.ui/test/unit/src/test/LogpointReviewMain.java new file mode 100644 index 000000000000..8588d21f761e --- /dev/null +++ b/java/debugger.jpda.ui/test/unit/src/test/LogpointReviewMain.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package test; + +public class LogpointReviewMain { + public static void main(String... args) { + Object[] keepAlive = {new Object(), new Object()}; + for (Object item : keepAlive) { + try { + throw new IllegalStateException(item.toString()); + } catch (IllegalStateException ex) { + System.out.println(ex.getMessage()); + } + } + } +} From 96ee9336108d4895a2258a623913d9f0b9932501 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Mon, 26 Oct 2026 07:07:12 +0100 Subject: [PATCH 07/13] Removing unused import --- .../modules/debugger/jpda/models/ObjectLocalVariable.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/ObjectLocalVariable.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/ObjectLocalVariable.java index 7db474f56dcd..53ed1b4e9d85 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/ObjectLocalVariable.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/ObjectLocalVariable.java @@ -21,7 +21,6 @@ import com.sun.jdi.AbsentInformationException; import com.sun.jdi.ClassNotLoadedException; -import com.sun.jdi.InvalidStackFrameException; import com.sun.jdi.InvalidTypeException; import com.sun.jdi.LocalVariable; import com.sun.jdi.ObjectReference; From 4db2f0a6d73f9fd1b90ec3f8b23744ca58584298 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Mon, 26 Oct 2026 07:25:21 +0100 Subject: [PATCH 08/13] Variable.toFixed() creates a snapshot of a variable --- java/api.debugger.jpda/apichanges.xml | 13 ++++ java/api.debugger.jpda/manifest.mf | 2 +- .../netbeans/api/debugger/jpda/Variable.java | 16 +++- java/debugger.jpda.ui/nbproject/project.xml | 2 +- .../debugger/jpda/ui/BreakpointOutput.java | 2 +- .../debugger/jpda/ui/FixedWatchesManager.java | 25 +++--- .../jpda/models/AbstractObjectVariable.java | 78 ------------------- .../jpda/models/AbstractVariable.java | 5 ++ 8 files changed, 44 insertions(+), 99 deletions(-) diff --git a/java/api.debugger.jpda/apichanges.xml b/java/api.debugger.jpda/apichanges.xml index aa0dc431f1ed..91396306a737 100644 --- a/java/api.debugger.jpda/apichanges.xml +++ b/java/api.debugger.jpda/apichanges.xml @@ -51,6 +51,19 @@ + + + Variable.toFixed + + + + + + New toFixed method added to request + an immutable snapshot of a current state of the variable. + + + ObjectInfoProvider, JPDABreakpointEvent, JPDADebugger enhancements diff --git a/java/api.debugger.jpda/manifest.mf b/java/api.debugger.jpda/manifest.mf index 52daef17d393..e733b72a644d 100644 --- a/java/api.debugger.jpda/manifest.mf +++ b/java/api.debugger.jpda/manifest.mf @@ -1,6 +1,6 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.api.debugger.jpda/2 OpenIDE-Module-Localizing-Bundle: org/netbeans/api/debugger/jpda/Bundle.properties -OpenIDE-Module-Specification-Version: 3.46 +OpenIDE-Module-Specification-Version: 3.47 OpenIDE-Module-Package-Dependencies: com.sun.jdi[VirtualMachineManager] diff --git a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/Variable.java b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/Variable.java index befe6b7dc1e8..1f604a6c2f6b 100644 --- a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/Variable.java +++ b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/Variable.java @@ -21,7 +21,7 @@ /** - * Represents some variable in debugged JVM. + * Represents some variable in the debuggee. * *
  * Since JDI interfaces evolve from one version to another, it's strongly recommended
@@ -61,5 +61,17 @@ public interface Variable {
      * @since 2.44
      */
     Object createMirrorObject();
-    
+
+    /** Creates animmutable snapshot of the variable.
+     * Clones the variable to assure that it is unique and sticks to its current JDI value.
+     * This is an optional operation. Variables that have no notion of
+     * snapshotting may return {@code this} as the default implementation
+     * of this method does.
+     *
+     * @return an immutable snapshot of the variable
+     * @since 3.47
+     */
+    default Variable toFixed() {
+        return this;
+    }
 }
diff --git a/java/debugger.jpda.ui/nbproject/project.xml b/java/debugger.jpda.ui/nbproject/project.xml
index 15bf0d2efcc3..a9769778ebeb 100644
--- a/java/debugger.jpda.ui/nbproject/project.xml
+++ b/java/debugger.jpda.ui/nbproject/project.xml
@@ -40,7 +40,7 @@
                     
                     
                         2
-                        3.45
+                        3.47
                     
                 
                 
diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java
index a7caa9fc08a0..55f20f02f8db 100644
--- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java
+++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java
@@ -470,7 +470,7 @@ final void printLocals(JPDABreakpointEvent event, BreakpointOutputWriter io) {
                     io.print("  ", null, false);
                     io.print(refValue.getName(), null, false);
                     io.print(" = ", null, false);
-                    if (refValue instanceof ObjectVariable obj && !isNull(obj)) {
+                    if (refValue instanceof ObjectVariable ov && !isNull(ov) && ov.toFixed() instanceof ObjectVariable obj) {
                         var sessionRef = new WeakReference<>(event.getDebugger().getSession());
                         io.print(obj.getValue(), () -> {
                             var s = sessionRef.get();
diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
index 6f1f86a54f9a..e733405774d5 100644
--- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
+++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
@@ -138,9 +138,9 @@ private boolean isPrimitive (Object node) {
     );
         
         
-    private Map             fixedWatches = new LinkedHashMap();
-    private HashSet         listeners;
-    private ContextProvider contextProvider;
+    private Map fixedWatches = new LinkedHashMap<>();
+    private HashSet listeners;
+    private final ContextProvider contextProvider;
 
     
     public FixedWatchesManager (ContextProvider contextProvider) {
@@ -148,8 +148,8 @@ public FixedWatchesManager (ContextProvider contextProvider) {
     }
 
     public void deleteAllFixedWatches() {
-        Collection nodes = new ArrayList(fixedWatches.keySet());
-        for (Iterator iter = nodes.iterator(); iter.hasNext();) {
+        Collection nodes = new ArrayList<>(fixedWatches.keySet());
+        for (Iterator iter = nodes.iterator(); iter.hasNext();) {
             fixedWatches.remove(iter.next());
             fireModelChanged(new ModelEvent.NodeChanged(FixedWatchesManager.this,
                 TreeModel.ROOT,
@@ -361,18 +361,11 @@ private void createFixedWatch (Object node) {
         }
     }
 
-    public void addFixedWatch (String name, Variable variable) {
+    public void addFixedWatch (String name, Variable orig) {
         // Clone the variable to assure that it's unique and sticks to the JDI value.
-        if (variable instanceof Cloneable) {
-            try { // terrible code to invoke the clone() method
-                java.lang.reflect.Method cloneMethod = variable.getClass().getMethod("clone", new Class[] {});
-                cloneMethod.setAccessible(true);
-                Object newVar = cloneMethod.invoke(variable, new Object[] {});
-                if (newVar instanceof Variable) {
-                    variable = (Variable) newVar;
-                }
-            } catch (Exception ex) {} // Ignore any exceptions
-        }
+        Variable variable =  orig instanceof ObjectVariable cloneable ?
+            cloneable.toFixed() :
+            orig;
         fixedWatches.put (new KeyWrapper(variable), name);
         fireModelChanged (new ModelEvent.NodeChanged(
                 this,
diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractObjectVariable.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractObjectVariable.java
index cd00586e5494..480ba5762d02 100644
--- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractObjectVariable.java
+++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractObjectVariable.java
@@ -1109,84 +1109,6 @@ public String toString () {
         return "ObjectVariable ";
     }
 
-    /*@Override
-    protected void finalize() throws Throwable {
-        Value v = getInnerValue();
-        if (v instanceof ObjectReference) {
-            try {
-                System.err.println("ENABLING collection for "+v);
-                ObjectReferenceWrapper.enableCollection((ObjectReference) v);
-            } catch (Exception ex) {}
-        }
-        super.finalize();
-    }*/
-
-    /* Uncomment when needed. Was used to create "readable" String and Char values.
-    private static String convertToStringInitializer (String s) {
-        StringBuffer sb = new StringBuffer ();
-        int i, k = s.length ();
-        for (i = 0; i < k; i++)
-            switch (s.charAt (i)) {
-                case '\b':
-                    sb.append ("\\b");
-                    break;
-                case '\f':
-                    sb.append ("\\f");
-                    break;
-                case '\\':
-                    sb.append ("\\\\");
-                    break;
-                case '\t':
-                    sb.append ("\\t");
-                    break;
-                case '\r':
-                    sb.append ("\\r");
-                    break;
-                case '\n':
-                    sb.append ("\\n");
-                    break;
-                case '\"':
-                    sb.append ("\\\"");
-                    break;
-                default:
-                    sb.append (s.charAt (i));
-            }
-        return sb.toString();
-    }
-
-    private static String convertToCharInitializer (String s) {
-        StringBuffer sb = new StringBuffer ();
-        int i, k = s.length ();
-        for (i = 0; i < k; i++)
-            switch (s.charAt (i)) {
-                case '\b':
-                    sb.append ("\\b");
-                    break;
-                case '\f':
-                    sb.append ("\\f");
-                    break;
-                case '\\':
-                    sb.append ("\\\\");
-                    break;
-                case '\t':
-                    sb.append ("\\t");
-                    break;
-                case '\r':
-                    sb.append ("\\r");
-                    break;
-                case '\n':
-                    sb.append ("\\n");
-                    break;
-                case '\'':
-                    sb.append ("\\\'");
-                    break;
-                default:
-                    sb.append (s.charAt (i));
-            }
-        return sb.toString();
-    }
-     */
-
     private class DebuggetStateListener extends Object implements PropertyChangeListener {
 
         @Override
diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractVariable.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractVariable.java
index 4dcc9a31b25c..48a8596ed1b7 100644
--- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractVariable.java
+++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractVariable.java
@@ -455,6 +455,11 @@ public Variable clone() {
         return clon;
     }
 
+    @Override
+    public Variable toFixed() {
+        return clone();
+    }
+
     @Override
     public Object createMirrorObject() {
         Value v = getJDIValue();

From 147bcc70b8840d67d8444e2f0685536b421af242 Mon Sep 17 00:00:00 2001
From: Jaroslav Tulach 
Date: Mon, 26 Oct 2026 07:34:38 +0100
Subject: [PATCH 09/13] Directly call toFixed()

---
 .../modules/debugger/jpda/ui/FixedWatchesManager.java         | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
index e733405774d5..037f2e4023d2 100644
--- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
+++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
@@ -363,9 +363,7 @@ private void createFixedWatch (Object node) {
 
     public void addFixedWatch (String name, Variable orig) {
         // Clone the variable to assure that it's unique and sticks to the JDI value.
-        Variable variable =  orig instanceof ObjectVariable cloneable ?
-            cloneable.toFixed() :
-            orig;
+        Variable variable =  orig.toFixed();
         fixedWatches.put (new KeyWrapper(variable), name);
         fireModelChanged (new ModelEvent.NodeChanged(
                 this,

From 8075fdce6f2f8bb357b120a286ff984e2aee4601 Mon Sep 17 00:00:00 2001
From: Jaroslav Tulach 
Date: Tue, 27 Oct 2026 07:44:15 +0100
Subject: [PATCH 10/13] Call it Variable.asValue

---
 java/api.debugger.jpda/apichanges.xml           |  6 +++---
 .../netbeans/api/debugger/jpda/Variable.java    | 17 ++++++++++-------
 .../debugger/jpda/ui/BreakpointOutput.java      |  2 +-
 .../debugger/jpda/ui/FixedWatchesManager.java   |  2 +-
 .../debugger/jpda/models/AbstractVariable.java  |  2 +-
 5 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/java/api.debugger.jpda/apichanges.xml b/java/api.debugger.jpda/apichanges.xml
index 91396306a737..8c685ec6b434 100644
--- a/java/api.debugger.jpda/apichanges.xml
+++ b/java/api.debugger.jpda/apichanges.xml
@@ -53,14 +53,14 @@
 
     
         
-        Variable.toFixed
+        Snapshot a value with Variable.asValue
         
         
         
         
         
-            New toFixed method added to request
-            an immutable snapshot of a current state of the variable.
+            New asValue method added to request
+            a snapshot of the current value of the variable.
         
         
     
diff --git a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/Variable.java b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/Variable.java
index 1f604a6c2f6b..2c6ededfbb5b 100644
--- a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/Variable.java
+++ b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/Variable.java
@@ -56,22 +56,25 @@ public interface Variable {
     /**
      * Create an object in this JVM, which mirrors the value of this variable in
      * the target JVM.
-     * 
+     *
      * @return The mirror object, or null when a mirror object can not be created.
      * @since 2.44
      */
     Object createMirrorObject();
 
-    /** Creates animmutable snapshot of the variable.
-     * Clones the variable to assure that it is unique and sticks to its current JDI value.
+    /**
+     * Creates a snapshot of the variable value. It clones the variable
+     * object to assure it sticks to its current debugee value
+     * regardless if the variable state gets re-assigned in the future.
+     *
      * This is an optional operation. Variables that have no notion of
-     * snapshotting may return {@code this} as the default implementation
-     * of this method does.
+     * snapshotting may return {@code this} as the default
+     * implementation of this method does.
      *
-     * @return an immutable snapshot of the variable
+     * @return a snapshot of the current variable value
      * @since 3.47
      */
-    default Variable toFixed() {
+    default Variable asValue() {
         return this;
     }
 }
diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java
index 55f20f02f8db..3dfaf3fcb00f 100644
--- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java
+++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/BreakpointOutput.java
@@ -470,7 +470,7 @@ final void printLocals(JPDABreakpointEvent event, BreakpointOutputWriter io) {
                     io.print("  ", null, false);
                     io.print(refValue.getName(), null, false);
                     io.print(" = ", null, false);
-                    if (refValue instanceof ObjectVariable ov && !isNull(ov) && ov.toFixed() instanceof ObjectVariable obj) {
+                    if (refValue instanceof ObjectVariable ov && !isNull(ov) && ov.asValue() instanceof ObjectVariable obj) {
                         var sessionRef = new WeakReference<>(event.getDebugger().getSession());
                         io.print(obj.getValue(), () -> {
                             var s = sessionRef.get();
diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
index 037f2e4023d2..4d3a9822e7e3 100644
--- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
+++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
@@ -363,7 +363,7 @@ private void createFixedWatch (Object node) {
 
     public void addFixedWatch (String name, Variable orig) {
         // Clone the variable to assure that it's unique and sticks to the JDI value.
-        Variable variable =  orig.toFixed();
+        Variable variable =  orig.asValue();
         fixedWatches.put (new KeyWrapper(variable), name);
         fireModelChanged (new ModelEvent.NodeChanged(
                 this,
diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractVariable.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractVariable.java
index 48a8596ed1b7..87439530c150 100644
--- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractVariable.java
+++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/models/AbstractVariable.java
@@ -456,7 +456,7 @@ public Variable clone() {
     }
 
     @Override
-    public Variable toFixed() {
+    public Variable asValue() {
         return clone();
     }
 

From e7a606dbf524a8b6d027e4ca3030c18f0ac38d1f Mon Sep 17 00:00:00 2001
From: Jaroslav Tulach 
Date: Tue, 27 Oct 2026 07:45:54 +0100
Subject: [PATCH 11/13] Needs latest version of api.debugger.jpda

---
 java/debugger.jpda/nbproject/project.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/java/debugger.jpda/nbproject/project.xml b/java/debugger.jpda/nbproject/project.xml
index 72d9cfe650b7..2b3afaf60d71 100644
--- a/java/debugger.jpda/nbproject/project.xml
+++ b/java/debugger.jpda/nbproject/project.xml
@@ -49,7 +49,7 @@
                     
                     
                         2
-                        3.18
+                        3.47
                     
                 
                 

From a86773c8aa794be67678e6472a7d2f5ff887561a Mon Sep 17 00:00:00 2001
From: Jaroslav Tulach 
Date: Tue, 27 Oct 2026 07:48:03 +0100
Subject: [PATCH 12/13] Removing unnecessary space

---
 .../netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
index 4d3a9822e7e3..39689bc4f24a 100644
--- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
+++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/FixedWatchesManager.java
@@ -363,7 +363,7 @@ private void createFixedWatch (Object node) {
 
     public void addFixedWatch (String name, Variable orig) {
         // Clone the variable to assure that it's unique and sticks to the JDI value.
-        Variable variable =  orig.asValue();
+        Variable variable = orig.asValue();
         fixedWatches.put (new KeyWrapper(variable), name);
         fireModelChanged (new ModelEvent.NodeChanged(
                 this,

From 87121f34864965eab8f842b3d28b6498f1a7a541 Mon Sep 17 00:00:00 2001
From: Jaroslav Tulach 
Date: Tue, 27 Oct 2026 12:59:15 +0100
Subject: [PATCH 13/13] Expanding tooltip to provide more info

---
 .../modules/debugger/jpda/ui/breakpoints/Bundle.properties    | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/Bundle.properties b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/Bundle.properties
index f5102fb01e63..599cab68cf13 100644
--- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/Bundle.properties
+++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/Bundle.properties
@@ -47,7 +47,9 @@
     L_Actions_Panel_Print_Text=Print &Text\:
 
     ACSD_TF_Actions_Panel_Print_Text=
-    TTT_TF_Actions_Panel_Print_Text=Text to be printed when breakpoint occurs. Use {=} to print expressions/variables.
+    TTT_TF_Actions_Panel_Print_Text=Text to be printed when breakpoint occurs.\n\
+         Use {=} to print expressions/variables.\n\
+        Use {className}, {methodName}, {lineNumber} or {locals} to print additional info.
 
 #ClassBreakpointPanel
     CTL_Class_Breakpoint_Print_Text=Class breakpoint hit for class {className}.