From beac4780d2285479de38825af91c3682b7b0ba88 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 18 Mar 2026 16:13:55 +0100 Subject: [PATCH 1/4] Breakpoints for lambdas. --- .../api/debugger/jpda/LineBreakpoint.java | 25 +- .../nbproject/project.xml | 8 + .../BreakpointAnnotationProvider.java | 22 +- .../jpda/projectsui/Bundle.properties | 2 + .../DebuggerBreakpointAnnotation.java | 12 +- .../projectsui/LambdaBreakpointManager.java | 279 ++++++++++++++++++ .../resources/DisabledLambdaBreakpoint.xml | 33 +++ .../jpda/resources/LambdaBreakpoint.xml | 36 +++ .../debugger/jpda/resources/mf-layer.xml | 2 + .../jpda/ui/breakpoints/Bundle.properties | 5 + .../ui/breakpoints/LineBreakpointPanel.form | 40 ++- .../ui/breakpoints/LineBreakpointPanel.java | 30 ++ .../jpda/ui/models/BreakpointsNodeModel.java | 21 +- .../debugger/jpda/ui/models/Bundle.properties | 1 + java/debugger.jpda/nbproject/project.xml | 3 +- .../jpda/breakpoints/BreakpointsReader.java | 5 + .../jpda/breakpoints/LineBreakpointImpl.java | 32 ++ java/java.lsp.server/nbproject/project.xml | 15 + .../server/debugging/NbProtocolServer.java | 81 +++++ .../breakpoints/BreakpointsManager.java | 99 +++++-- .../debugging/breakpoints/NbBreakpoint.java | 50 +++- .../NbBreakpointsRequestHandler.java | 4 +- 22 files changed, 758 insertions(+), 47 deletions(-) create mode 100644 java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/LambdaBreakpointManager.java create mode 100644 java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/DisabledLambdaBreakpoint.xml create mode 100644 java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/LambdaBreakpoint.xml diff --git a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/LineBreakpoint.java b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/LineBreakpoint.java index 78726be196d7..d073ac961258 100644 --- a/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/LineBreakpoint.java +++ b/java/api.debugger.jpda/src/org/netbeans/api/debugger/jpda/LineBreakpoint.java @@ -67,6 +67,8 @@ public class LineBreakpoint extends JPDABreakpoint { /** Property name constant */ public static final String PROP_LINE_NUMBER = "lineNumber"; // NOI18N /** Property name constant */ + public static final String PROP_LAMBDA_INDEX = "lambdaIndex"; // NOI18N + /** Property name constant */ public static final String PROP_URL = "url"; // NOI18N /** Property name constant. */ public static final String PROP_CONDITION = "condition"; // NOI18N @@ -94,6 +96,7 @@ public class LineBreakpoint extends JPDABreakpoint { private String className = null; private Map instanceFilters; private Map threadFilters; + private int lambdaIndex = -1; //line breakpoint by default private LineBreakpoint (String url) { @@ -182,6 +185,26 @@ public void setLineNumber (int ln) { Integer.valueOf(ln) ); } + + public int getLambdaIndex() { + return lambdaIndex; + } + + public void setLambdaIndex(int li) { + int old; + synchronized (this) { + if (li == lambdaIndex) { + return; + } + old = lambdaIndex; + lambdaIndex = li; + } + firePropertyChange ( + PROP_LAMBDA_INDEX, + Integer.valueOf(old), + Integer.valueOf(li) + ); + } /** * Get the instance filter for a specific debugger session. @@ -423,7 +446,7 @@ public String toString () { if (fileName == null) { fileName = url; } - return "LineBreakpoint " + fileName + " : " + lineNumber; + return "LineBreakpoint " + fileName + " : " + lineNumber + (lambdaIndex >= 0 ? " lambda index: " + lambdaIndex : ""); } private static class LineBreakpointImpl extends LineBreakpoint diff --git a/java/debugger.jpda.projectsui/nbproject/project.xml b/java/debugger.jpda.projectsui/nbproject/project.xml index 561252c26711..6790eca564da 100644 --- a/java/debugger.jpda.projectsui/nbproject/project.xml +++ b/java/debugger.jpda.projectsui/nbproject/project.xml @@ -146,6 +146,14 @@ 1.62 + + org.netbeans.modules.java.source + + + + 0.191 + + org.netbeans.modules.java.source.base diff --git a/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/BreakpointAnnotationProvider.java b/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/BreakpointAnnotationProvider.java index 2ca1022f3580..6f6eb0f76eac 100644 --- a/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/BreakpointAnnotationProvider.java +++ b/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/BreakpointAnnotationProvider.java @@ -139,6 +139,7 @@ private void annotate (final FileObject fo) { } annotatedFiles.add(fo); } + LambdaBreakpointManager.FactoryImpl.doRefresh(fo); //ensure spans of lambda breakpoints are updated } void setBreakpointsActive(boolean active) { @@ -189,6 +190,7 @@ private void refreshAnnotation(JPDABreakpoint b) { breakpointToAnnotations.put(b, Collections.newSetFromMap(new WeakHashMap<>())); for (FileObject fo : annotatedFiles) { addAnnotationTo(b, fo); + LambdaBreakpointManager.FactoryImpl.doRefresh(fo); } } } @@ -207,12 +209,17 @@ private static String getAnnotationType(JPDABreakpoint b, boolean isConditional, boolean active) { boolean isInvalid = b.getValidity() == VALIDITY.INVALID; String annotationType; - if (b instanceof LineBreakpoint) { - annotationType = b.isEnabled () ? - (isConditional ? EditorContext.CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE : - EditorContext.BREAKPOINT_ANNOTATION_TYPE) : - (isConditional ? EditorContext.DISABLED_CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE : - EditorContext.DISABLED_BREAKPOINT_ANNOTATION_TYPE); + if (b instanceof LineBreakpoint lb) { + if (lb.getLambdaIndex() >= 0) { + annotationType = b.isEnabled() ? DebuggerBreakpointAnnotation.LAMBDA_BREAKPOINT + : DebuggerBreakpointAnnotation.DISABLED_LAMBDA_BREAKPOINT; + } else { + annotationType = b.isEnabled () ? + (isConditional ? EditorContext.CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE : + EditorContext.BREAKPOINT_ANNOTATION_TYPE) : + (isConditional ? EditorContext.DISABLED_CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE : + EditorContext.DISABLED_BREAKPOINT_ANNOTATION_TYPE); + } } else if (b instanceof FieldBreakpoint) { annotationType = b.isEnabled () ? EditorContext.FIELD_BREAKPOINT_ANNOTATION_TYPE : @@ -475,6 +482,9 @@ static String getCondition(Breakpoint b) { } } + Set getAnnotationsForBreakpoint(JPDABreakpoint b) { + return breakpointToAnnotations.getOrDefault(b, Set.of()); + } /* // Not used @Override diff --git a/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/Bundle.properties b/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/Bundle.properties index 102e8b85d3a4..f9220ce54e27 100644 --- a/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/Bundle.properties +++ b/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/Bundle.properties @@ -41,6 +41,8 @@ TOOLTIP_DISABLED_METHOD_BREAKPOINT=Disabled Method Breakpoint TOOLTIP_CLASS_BREAKPOINT=Class Breakpoint TOOLTIP_DISABLED_CLASS_BREAKPOINT=Disabled Class Breakpoint TOOLTIP_BREAKPOINT_STROKE=Deactivated breakpoint +TOOLTIP_LAMBDA_BREAKPOINT=Lambda Breakpoint +TOOLTIP_DISABLED_LAMBDA_BREAKPOINT=Lambda Breakpoint # A condition expression follows this message on a next line TOOLTIP_CONDITION=Hits when: # {0} - a number diff --git a/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/DebuggerBreakpointAnnotation.java b/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/DebuggerBreakpointAnnotation.java index 5e0abec4496d..6159bb69bae0 100644 --- a/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/DebuggerBreakpointAnnotation.java +++ b/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/DebuggerBreakpointAnnotation.java @@ -23,7 +23,6 @@ import java.util.List; import org.netbeans.api.debugger.Breakpoint; import org.netbeans.api.debugger.Breakpoint.HIT_COUNT_FILTERING_STYLE; -import org.netbeans.api.debugger.jpda.JPDABreakpoint; import org.netbeans.spi.debugger.jpda.EditorContext; import org.netbeans.spi.debugger.ui.BreakpointAnnotation; @@ -39,6 +38,9 @@ */ public class DebuggerBreakpointAnnotation extends BreakpointAnnotation { + public static final String LAMBDA_BREAKPOINT = "LambdaBreakpoint"; + public static final String DISABLED_LAMBDA_BREAKPOINT = "DisabledLambdaBreakpoint"; + private final Line line; private final String type; private final Breakpoint breakpoint; @@ -163,6 +165,14 @@ private String getShortDescriptionIntern () { if (type == EditorContext.DISABLED_CLASS_BREAKPOINT_ANNOTATION_TYPE) { return NbBundle.getMessage (DebuggerBreakpointAnnotation.class, "TOOLTIP_DISABLED_CLASS_BREAKPOINT"); // NOI18N + } else + if (type == LAMBDA_BREAKPOINT) { + return NbBundle.getMessage + (DebuggerBreakpointAnnotation.class, "TOOLTIP_LAMBDA_BREAKPOINT"); // NOI18N + } else + if (type == DISABLED_LAMBDA_BREAKPOINT) { + return NbBundle.getMessage + (DebuggerBreakpointAnnotation.class, "TOOLTIP_DISABLED_LAMBDA_BREAKPOINT"); // NOI18N } ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, new IllegalStateException("Unknown breakpoint type '"+type+"'.")); return null; diff --git a/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/LambdaBreakpointManager.java b/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/LambdaBreakpointManager.java new file mode 100644 index 000000000000..d394253b8c46 --- /dev/null +++ b/java/debugger.jpda.projectsui/src/org/netbeans/modules/debugger/jpda/projectsui/LambdaBreakpointManager.java @@ -0,0 +1,279 @@ +/* + * 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.projectsui; + +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.Tree; +import java.beans.PropertyChangeEvent; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import org.netbeans.api.debugger.Breakpoint; +import org.netbeans.api.debugger.DebuggerEngine; +import org.netbeans.api.debugger.DebuggerManager; +import org.netbeans.api.debugger.DebuggerManagerAdapter; +import org.netbeans.api.debugger.LazyDebuggerManagerListener; +import org.netbeans.api.debugger.jpda.JPDABreakpoint; +import org.netbeans.api.debugger.jpda.JPDADebugger; +import org.netbeans.api.debugger.jpda.LineBreakpoint; +import org.netbeans.api.java.source.CancellableTask; +import org.netbeans.api.java.source.CompilationInfo; +import org.netbeans.api.java.source.JavaSource.Phase; +import org.netbeans.api.java.source.JavaSource.Priority; +import org.netbeans.api.java.source.JavaSourceTaskFactory; +import org.netbeans.api.java.source.support.CancellableTreePathScanner; +import org.netbeans.api.java.source.support.EditorAwareJavaSourceTaskFactory; +import org.netbeans.spi.debugger.DebuggerServiceRegistration; +import org.openide.cookies.LineCookie; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.URLMapper; +import org.openide.text.Annotation; +import org.openide.text.Line; +import org.openide.text.Line.Part; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.lookup.ServiceProvider; + +@DebuggerServiceRegistration(types=LazyDebuggerManagerListener.class) +public class LambdaBreakpointManager extends DebuggerManagerAdapter { + + private static volatile JPDADebugger currentDebugger = null; //XXX: static??? + + @Override + public String[] getProperties() { + return new String[] { DebuggerManager.PROP_BREAKPOINTS, DebuggerManager.PROP_DEBUGGER_ENGINES }; + } + + @Override + public void breakpointAdded(Breakpoint breakpoint) { + refreshAfterChange(breakpoint); + } + + @Override + public void breakpointRemoved(Breakpoint breakpoint) { + refreshAfterChange(breakpoint); + } + + private void refreshAfterChange(Breakpoint breakpoint) { + if (breakpoint instanceof LineBreakpoint lb) { + try { + URL currentURL = new URL(lb.getURL()); + FileObject fo = URLMapper.findFileObject(currentURL); + + if (fo != null) { + FactoryImpl.doRefresh(fo); + } + } catch (MalformedURLException ex) { + Exceptions.printStackTrace(ex); + } + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + String propertyName = evt.getPropertyName (); + if (propertyName == null) { + return; + } + if (DebuggerManager.PROP_CURRENT_ENGINE.equals(propertyName)) { + setCurrentDebugger(DebuggerManager.getDebuggerManager().getCurrentEngine()); + } + if ( (!LineBreakpoint.PROP_URL.equals (propertyName)) && + (!LineBreakpoint.PROP_LINE_NUMBER.equals (propertyName)) + ) { + return; + } + JPDABreakpoint b = (JPDABreakpoint) evt.getSource (); + DebuggerManager manager = DebuggerManager.getDebuggerManager(); + Breakpoint[] bkpts = manager.getBreakpoints(); + boolean found = false; + for (int x = 0; x < bkpts.length; x++) { + if (b == bkpts[x]) { + found = true; + break; + } + } + if (!found) { + // breakpoint has been removed + return; + } + } + + private void setCurrentDebugger(DebuggerEngine engine) { + JPDADebugger oldDebugger = currentDebugger; + if (oldDebugger != null) { + oldDebugger.removePropertyChangeListener(JPDADebugger.PROP_BREAKPOINTS_ACTIVE, this); + } + boolean active = true; + JPDADebugger debugger = null; + if (engine != null) { + debugger = engine.lookupFirst(null, JPDADebugger.class); + if (debugger != null) { + debugger.addPropertyChangeListener(JPDADebugger.PROP_BREAKPOINTS_ACTIVE, this); + active = debugger.getBreakpointsActive(); + } + } + currentDebugger = debugger; + } + + //TODO: requires dependency on java.source - maybe try to rewrite to Schedulers! + @ServiceProvider(service=JavaSourceTaskFactory.class) + public static final class FactoryImpl extends EditorAwareJavaSourceTaskFactory { + + private BreakpointAnnotationProvider bap; + + public FactoryImpl() { + super(Phase.PARSED, Priority.BELOW_NORMAL); + } + + private BreakpointAnnotationProvider getAnnotationProvider() { + if (bap == null) { + bap = BreakpointAnnotationProvider.getInstance(); + } + return bap; + } + + @Override + protected CancellableTask createTask(FileObject file) { + return new CancellableTask() { + private final AtomicBoolean canceled = new AtomicBoolean(); + + @Override + public void cancel() { + canceled.set(true); + } + + @Override + public void run(CompilationInfo info) throws Exception { + canceled.set(false); + + String currentFile = info.getFileObject().toURL().toString(); + Map> lines2LambdaIndexes = new HashMap<>(); + + for (Breakpoint b : DebuggerManager.getDebuggerManager().getBreakpoints()) { + if (b instanceof LineBreakpoint lb && currentFile.equals(lb.getURL())) { + lines2LambdaIndexes.computeIfAbsent(lb.getLineNumber(), x -> new HashMap<>()) + .put(lb.getLambdaIndex(), lb); + } + } + + LineMap lineMap = info.getCompilationUnit().getLineMap(); + + new CancellableTreePathScanner(canceled) { + int currentLine = -1; + int currentLambdaIndex = -1; + public Void scan(Tree tree, Void v) { + if (tree != null && tree.getKind() != Tree.Kind.COMPILATION_UNIT) { + long startPos = info.getTrees().getSourcePositions().getStartPosition(getCurrentPath().getCompilationUnit(), tree); + if (startPos != (-1)) { + int line = (int) lineMap.getLineNumber(startPos); + + if (line != currentLine) { + currentLine = line; + currentLambdaIndex = 0; + } + } + } + return super.scan(tree, v); + } + public Void visitLambdaExpression(LambdaExpressionTree tree, Void v) { + long startPos = info.getTrees().getSourcePositions().getStartPosition(getCurrentPath().getCompilationUnit(), tree); + int startLine = (int) lineMap.getLineNumber(startPos); + Map existingLineBreakpoints = lines2LambdaIndexes.get(startLine); + LineBreakpoint existingLineBreakpoint = existingLineBreakpoints != null ? existingLineBreakpoints.remove(currentLambdaIndex) : null; + + if (existingLineBreakpoints != null && existingLineBreakpoints.containsKey(-1)) { + //the line breakpoint exists, ensure the lambda breakpoint exists and is updated + if (existingLineBreakpoint == null) { + LineBreakpoint lb = LineBreakpoint.create(currentFile, startLine); + lb.setLambdaIndex(currentLambdaIndex); + lb.disable(); + DebuggerManager.getDebuggerManager().addBreakpoint(lb); + } else { + long endPos = info.getTrees().getSourcePositions().getEndPosition(getCurrentPath().getCompilationUnit(), tree); + int endLine = (int) lineMap.getLineNumber(endPos); + int startColumn = (int) lineMap.getColumnNumber(startPos) - 1; + int endColumn; + if (startLine == endLine) { + endColumn = (int) lineMap.getColumnNumber(endPos) - 1; + } else { + endColumn = (int) lineMap.getStartPosition(startLine + 1) - 1; + } + + int length = endColumn - startColumn; + + Set annotations = getAnnotationProvider().getAnnotationsForBreakpoint(existingLineBreakpoint); + + for (Annotation ann : annotations) { + if (!(ann.getAttachedAnnotatable() instanceof Part p) || p.getLine().getLineNumber() != startLine || p.getColumn() != startColumn || p.getLength() != length) { + LineCookie lc = info.getFileObject().getLookup().lookup(LineCookie.class); + + if (lc == null) { + continue; + } + + Line line = lc.getLineSet().getCurrent(startLine - 1); + Part part = line.createPart(startColumn, length); + + if (canceled.get()) { + //don't set the part if cancelled - the positions may be wrong + return null; + } + + ann.detach(); + ann.attach(part); + } + } + } + } + + currentLambdaIndex++; + + return super.visitLambdaExpression(tree, v); + } + }.scan(info.getCompilationUnit(), null); + + //remove any stale lambda breakpoints: + for (Breakpoint b : DebuggerManager.getDebuggerManager().getBreakpoints()) { + if (b instanceof LineBreakpoint lb && currentFile.equals(lb.getURL()) && lb.getLambdaIndex() >=0) { + Map staleLambdaBreakpoints = lines2LambdaIndexes.getOrDefault(lb.getLineNumber(), Map.of()); + if (staleLambdaBreakpoints.containsKey(lb.getLambdaIndex()) || !staleLambdaBreakpoints.containsKey(-1)) { + DebuggerManager.getDebuggerManager().removeBreakpoint(lb); + } + } + } + } + }; + } + + public static void doRefresh(FileObject file) { + for (JavaSourceTaskFactory f : Lookup.getDefault().lookupAll(JavaSourceTaskFactory.class)) { + if (f instanceof FactoryImpl impl) { + impl.reschedule(file); + } + } + } + } + +} diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/DisabledLambdaBreakpoint.xml b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/DisabledLambdaBreakpoint.xml new file mode 100644 index 000000000000..e841d70ba90b --- /dev/null +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/DisabledLambdaBreakpoint.xml @@ -0,0 +1,33 @@ + + + + + diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/LambdaBreakpoint.xml b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/LambdaBreakpoint.xml new file mode 100644 index 000000000000..6a83877f77ba --- /dev/null +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/LambdaBreakpoint.xml @@ -0,0 +1,36 @@ + + + + + diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/mf-layer.xml b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/mf-layer.xml index df823ccc2467..4f05e7f2004a 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/mf-layer.xml +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/mf-layer.xml @@ -91,9 +91,11 @@ + + 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 fe41db22e26f..d21a239259c2 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 @@ -229,6 +229,11 @@ ACSD_TF_Line_Breakpoint_Line_Number=Line number TTT_TF_Line_Breakpoint_Line_Number=Line number to stop at + L_Line_Breakpoint_Lambda_Index=Lambda &Index\: + ACSD_L_Line_Breakpoint_Lambda_Index=Lambda Index + ACSD_TF_Line_Breakpoint_Lambda_Index=Lambda Index + TTT_TF_Line_Breakpoint_Lambda_Index=Lambda Index to stop at + L_Line_Breakpoint_Condition=Co&ndition: ACSD_L_Line_Breakpoint_Condition=Condition ACSD_TF_Line_Breakpoint_Condition=Condition diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/LineBreakpointPanel.form b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/LineBreakpointPanel.form index 5ea1b1c1ff72..9eb4f7f3a2d4 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/LineBreakpointPanel.form +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/LineBreakpointPanel.form @@ -1,4 +1,4 @@ - + - - - diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/LambdaBreakpoint.xml b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/LambdaBreakpoint.xml deleted file mode 100644 index 6a83877f77ba..000000000000 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/LambdaBreakpoint.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/mf-layer.xml b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/mf-layer.xml index 4f05e7f2004a..df823ccc2467 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/mf-layer.xml +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/resources/mf-layer.xml @@ -91,11 +91,9 @@ - - diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/LineBreakpointPanel.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/LineBreakpointPanel.java index b36d6c855bf5..50fc0d975053 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/LineBreakpointPanel.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/breakpoints/LineBreakpointPanel.java @@ -25,8 +25,10 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; @@ -130,7 +132,7 @@ public LineBreakpointPanel (LineBreakpoint b, boolean createBreakpoint) { tfFileName.getPreferredSize().height)); tfLineNumber.setText(Integer.toString(b.getLineNumber())); - tfLambdaIndex.setText(Integer.toString(b.getLambdaIndex())); + tfLambdaIndex.setText(Arrays.stream(b.getLambdaIndex()).mapToObj(String::valueOf).collect(Collectors.joining(", "))); conditionsPanel = new ConditionsPanel(HELP_ID); setupConditionPane(); conditionsPanel.showClassFilter(false); @@ -348,7 +350,12 @@ public boolean ok () { logger.fine(" => URL = '"+url+"'"); breakpoint.setURL((url != null) ? url.toString() : path); breakpoint.setLineNumber(Integer.parseInt(tfLineNumber.getText().trim())); - breakpoint.setLambdaIndex(Integer.parseInt(tfLambdaIndex.getText().trim())); + String lambdaIndexText = tfLambdaIndex.getText().trim(); + if (lambdaIndexText.isEmpty()) { + breakpoint.setLambdaIndex(new int[0]); + } else { + breakpoint.setLambdaIndex(Arrays.stream(lambdaIndexText.split(", *")).mapToInt(v -> Integer.parseInt(v)).toArray()); + } breakpoint.setCondition (conditionsPanel.getCondition()); breakpoint.setHitCountFilter(conditionsPanel.getHitCount(), conditionsPanel.getHitCountFilteringStyle()); diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/BreakpointsNodeModel.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/BreakpointsNodeModel.java index 0131536307ee..d0710b4a2330 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/BreakpointsNodeModel.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/BreakpointsNodeModel.java @@ -19,9 +19,11 @@ package org.netbeans.modules.debugger.jpda.ui.models; +import java.util.Arrays; import java.util.Map; import java.util.Vector; import java.util.WeakHashMap; +import java.util.stream.Collectors; import org.netbeans.api.debugger.Breakpoint; import org.netbeans.api.debugger.Breakpoint.VALIDITY; import org.netbeans.api.debugger.DebuggerManager; @@ -107,13 +109,18 @@ public String getDisplayName (Object o) throws UnknownTypeException { } return bold ( b, - b.getLambdaIndex() >= 0 ? + b.getLambdaIndex().length > 0 ? NbBundle.getMessage ( BreakpointsNodeModel.class, "CTL_Line_Lambda_Breakpoint", EditorContextBridge.getFileName (b), line, - b.getLambdaIndex() + Arrays.stream(b.getLambdaIndex()) + .mapToObj(i -> switch (i) { + case LineBreakpoint.LAMBDA_INDEX_STOP_OUTSIDE -> NbBundle.getMessage(BreakpointsNodeModel.class, "CTL_Line_Lambda_Breakpoint_Outside"); + default -> NbBundle.getMessage(BreakpointsNodeModel.class, "CTL_Line_Lambda_Breakpoint_OnLambda", i + 1); + }) + .collect(Collectors.joining(", ")) ) : NbBundle.getMessage ( diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/Bundle.properties b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/Bundle.properties index b48aa4aefcce..d8126856025d 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/Bundle.properties +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/Bundle.properties @@ -23,7 +23,9 @@ ACSD_Breakpoint_Customizer_Dialog=Customize this breakpoint's properties #BreakpointsNodeModel CTL_Line_Breakpoint=Line {0}:{1} -CTL_Line_Lambda_Breakpoint=Line {0}:{1}, lambda index: {2} +CTL_Line_Lambda_Breakpoint=Line {0}:{1}, stopping: {2} +CTL_Line_Lambda_Breakpoint_Outside=outside lambdas +CTL_Line_Lambda_Breakpoint_OnLambda=on {0}. lambda CTL_Thread_Started_Breakpoint=Thread started CTL_Thread_Death_Breakpoint=Thread death CTL_Thread_Breakpoint=Thread start / death diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/breakpoints/BreakpointsReader.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/breakpoints/BreakpointsReader.java index 659b7b0ee688..f0204ca865de 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/breakpoints/BreakpointsReader.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/breakpoints/BreakpointsReader.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -108,7 +109,7 @@ public Object read (String typeID, Properties properties) { LineBreakpoint lb = LineBreakpoint.create ("", 0); lb.setURL(properties.getString (LineBreakpoint.PROP_URL, null)); lb.setLineNumber(properties.getInt (LineBreakpoint.PROP_LINE_NUMBER, 1)); - lb.setLambdaIndex(properties.getInt (LineBreakpoint.PROP_LAMBDA_INDEX, -1)); + lb.setLambdaIndex(Arrays.stream(properties.getArray(LineBreakpoint.PROP_LAMBDA_INDEX, new Object[0])).mapToInt(v -> (Integer) v).toArray()); lb.setCondition ( properties.getString (LineBreakpoint.PROP_CONDITION, "") ); @@ -371,9 +372,9 @@ public void write (Object object, Properties properties) { LineBreakpoint.PROP_LINE_NUMBER, lb.getLineNumber () ); - properties.setInt ( + properties.setArray( LineBreakpoint.PROP_LAMBDA_INDEX, - lb.getLambdaIndex () + Arrays.stream(lb.getLambdaIndex()).mapToObj(v -> v).toArray() ); properties.setString ( LineBreakpoint.PROP_CONDITION, diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/breakpoints/LineBreakpointImpl.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/breakpoints/LineBreakpointImpl.java index b3e9e22dac68..ba65c249686d 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/breakpoints/LineBreakpointImpl.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/breakpoints/LineBreakpointImpl.java @@ -48,6 +48,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -96,6 +97,7 @@ import org.openide.util.Exceptions; import org.openide.util.NbBundle; import java.util.LinkedHashMap; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import static java.util.stream.Collectors.collectingAndThen; @@ -116,7 +118,7 @@ public class LineBreakpointImpl extends ClassBasedBreakpoint { private int lineNumber; private int breakpointLineNumber; - private int lambdaIndex; + private int[] lambdaIndex; private int lineNumberForUpdate = -1; private final Object lineLock = new Object(); private BreakpointsReader reader; @@ -145,7 +147,7 @@ private void updateLineNumber() { lb, getDebugger()); int lbln = lb.getLineNumber(); - int li = lb.getLambdaIndex(); + int[] li = lb.getLambdaIndex(); synchronized (lineLock) { breakpointLineNumber = lbln; lineNumber = theLineNumber; @@ -327,7 +329,7 @@ protected void classLoaded (List referenceTypes) { String failReason = null; ReferenceType noLocRefType = null; int lineNumberToSet; - int lambdaIndexToSet; + int[] lambdaIndexToSet; final int origBreakpointLineNumber; int newBreakpointLineNumber; synchronized (lineLock) { @@ -707,25 +709,36 @@ private static String normalize(String path) { return path; } - private List filterLocationsByLambdaIndex(List locations, int lambdaIndex) { - if (lambdaIndex == Integer.MIN_VALUE) { + private List filterLocationsByLambdaIndex(List locations, int[] lambdaIndex) { + if (lambdaIndex.length == 0) { return locations; - } else if (lambdaIndex == (-1)) { - return locations.stream() - .filter(l -> !l.method().name().startsWith("lambda$")) - .collect(Collectors.toList()); } else { - List filtered = - locations.stream() - .filter(l -> l.method().name().startsWith("lambda$")) - .collect(Collectors.toList()); + Map> lambda2Locations = new LinkedHashMap<>(); + List outsideOfLambda = new ArrayList<>(); - Collections.reverse(filtered); - if (lambdaIndex < filtered.size()) { - return Collections.singletonList(filtered.get(lambdaIndex)); - } else { - return filtered; + for (Location l : locations) { + if (l.method().name().startsWith("lambda$")) { + lambda2Locations.computeIfAbsent(l.method().name(), k -> new ArrayList<>()).add(l); + } else { + outsideOfLambda.add(l); + } } + + List lambdas = new ArrayList<>(lambda2Locations.keySet()); + + Collections.reverse(lambdas); + + List result = new ArrayList<>(); + + for (int index : lambdaIndex) { + if (index == LineBreakpoint.LAMBDA_INDEX_STOP_OUTSIDE) { + result.addAll(outsideOfLambda); + } else if (index >= 0 && index < lambdas.size()) { + result.addAll(lambda2Locations.get(lambdas.get(index))); + } + } + + return result; } } diff --git a/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/ExpressionLambdaBreakpointTest.java b/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/ExpressionLambdaBreakpointTest.java index 643ffdaee29f..bdb3fa68d3b6 100644 --- a/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/ExpressionLambdaBreakpointTest.java +++ b/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/ExpressionLambdaBreakpointTest.java @@ -40,11 +40,13 @@ */ public class ExpressionLambdaBreakpointTest extends NbTestCase { - private static final String TEST_APP_PATH = System.getProperty ("test.dir.src") + + private static final String TEST_APP_PATH = System.getProperty ("test.dir.src") + "org/netbeans/api/debugger/jpda/testapps/ExpressionLambdaBreakpointApp.java"; - + private static final String TEST_MULTI_LINE_APP_PATH = System.getProperty ("test.dir.src") + + "org/netbeans/api/debugger/jpda/testapps/ExpressionLambdaBreakpointMultiLineApp.java"; + private JPDASupport support; - + public ExpressionLambdaBreakpointTest (String s) { super (s); } @@ -52,13 +54,13 @@ public ExpressionLambdaBreakpointTest (String s) { public static Test suite() { return JPDASupport.createTestSuite(ExpressionLambdaBreakpointTest.class); } - + public void testLambdaBreakpointsStopAll() throws Exception { try { Utils.BreakPositions bp = Utils.getBreakPositions(TEST_APP_PATH); LineBreakpoint[] lb = bp.getBreakpoints().toArray(new LineBreakpoint[0]); - lb[0].setLambdaIndex(Integer.MIN_VALUE); //stop on all locations + lb[0].setLambdaIndex(new int[0]); //stop on all locations DebuggerManager dm = DebuggerManager.getDebuggerManager (); for (int i = 0; i < lb.length; i++) { @@ -73,33 +75,33 @@ public void testLambdaBreakpointsStopAll() throws Exception { support = JPDASupport.attach ( "org.netbeans.api.debugger.jpda.testapps.ExpressionLambdaBreakpointApp" ); - + JPDADebugger debugger = support.getDebugger(); int lambdaBpLineHitCount = 6; //total list vaues + 1 for (int j = 0; j < lambdaBpLineHitCount; j++) { support.waitState (JPDADebugger.STATE_STOPPED); // j-th breakpoint hit assertEquals ( - "Debugger stopped at wrong line for breakpoint", - lb[0].getLineNumber (), + "Debugger stopped at wrong line for breakpoint", + lb[0].getLineNumber (), debugger.getCurrentCallStackFrame ().getLineNumber (null) ); - + if (j == 0) { support.stepOver(); assertEquals ( - "Debugger stopped at wrong line for breakpoint", - lb[0].getLineNumber ()+ 1, + "Debugger stopped at wrong line for breakpoint", + lb[0].getLineNumber ()+ 1, debugger.getCurrentCallStackFrame ().getLineNumber (null) ); } support.doContinue(); } - + support.waitState (JPDADebugger.STATE_STOPPED); - + assertEquals ( - "Debugger stopped at wrong line for breakpoint", - lb[1].getLineNumber (), + "Debugger stopped at wrong line for breakpoint", + lb[1].getLineNumber (), debugger.getCurrentCallStackFrame ().getLineNumber (null) ); @@ -111,7 +113,7 @@ public void testLambdaBreakpointsStopAll() throws Exception { } Map variablesByName = getVariablesByName(debugger.getCurrentCallStackFrame ().getLocalVariables()); assertTrue("Wrong computation value of lambda expression filter",checkMirrorValues(variablesByName, new Object[]{"a","b","c"})); - + support.doContinue (); support.waitState (JPDADebugger.STATE_DISCONNECTED); } finally { @@ -124,7 +126,7 @@ public void testLambdaBreakpointsStopAtLambda() throws Exception { Utils.BreakPositions bp = Utils.getBreakPositions(TEST_APP_PATH); LineBreakpoint[] lb = bp.getBreakpoints().toArray(new LineBreakpoint[0]); - lb[0].setLambdaIndex(0); //stop inside lambda + lb[0].setLambdaIndex(new int[] {0}); //stop inside lambda DebuggerManager dm = DebuggerManager.getDebuggerManager (); for (int i = 0; i < lb.length; i++) { @@ -139,14 +141,14 @@ public void testLambdaBreakpointsStopAtLambda() throws Exception { support = JPDASupport.attach ( "org.netbeans.api.debugger.jpda.testapps.ExpressionLambdaBreakpointApp" ); - + JPDADebugger debugger = support.getDebugger(); Object[] expectedValues = new Object[]{"a", "", "b", "", "c"}; for (int j = 0; j < expectedValues.length; j++) { support.waitState (JPDADebugger.STATE_STOPPED); // j-th breakpoint hit assertEquals ( - "Debugger stopped at wrong line for breakpoint", - lb[0].getLineNumber (), + "Debugger stopped at wrong line for breakpoint", + lb[0].getLineNumber (), debugger.getCurrentCallStackFrame ().getLineNumber (null) ); @@ -156,12 +158,12 @@ public void testLambdaBreakpointsStopAtLambda() throws Exception { support.doContinue(); } - + support.waitState (JPDADebugger.STATE_STOPPED); - + assertEquals ( - "Debugger stopped at wrong line for breakpoint", - lb[1].getLineNumber (), + "Debugger stopped at wrong line for breakpoint", + lb[1].getLineNumber (), debugger.getCurrentCallStackFrame ().getLineNumber (null) ); @@ -171,7 +173,7 @@ public void testLambdaBreakpointsStopAtLambda() throws Exception { for (int i = 0; i < tb.length; i++) { dm.removeBreakpoint (lb[i]); } - + support.doContinue (); support.waitState (JPDADebugger.STATE_DISCONNECTED); } finally { @@ -184,7 +186,7 @@ public void testLambdaBreakpointsStopOutsideOfLambda() throws Exception { Utils.BreakPositions bp = Utils.getBreakPositions(TEST_APP_PATH); LineBreakpoint[] lb = bp.getBreakpoints().toArray(new LineBreakpoint[0]); - lb[0].setLambdaIndex(-1); //stop outside lambda + lb[0].setLambdaIndex(new int[] {LineBreakpoint.LAMBDA_INDEX_STOP_OUTSIDE}); //stop outside lambda DebuggerManager dm = DebuggerManager.getDebuggerManager (); for (int i = 0; i < lb.length; i++) { @@ -199,13 +201,71 @@ public void testLambdaBreakpointsStopOutsideOfLambda() throws Exception { support = JPDASupport.attach ( "org.netbeans.api.debugger.jpda.testapps.ExpressionLambdaBreakpointApp" ); - + JPDADebugger debugger = support.getDebugger(); support.waitState (JPDADebugger.STATE_STOPPED); // j-th breakpoint hit assertEquals ( - "Debugger stopped at wrong line for breakpoint", - lb[0].getLineNumber (), + "Debugger stopped at wrong line for breakpoint", + lb[0].getLineNumber (), + debugger.getCurrentCallStackFrame ().getLineNumber (null) + ); + + Map variablesByName = getVariablesByName(debugger.getCurrentCallStackFrame ().getLocalVariables()); + + assertEquals(String.valueOf(variablesByName), Set.of("args"), variablesByName.keySet()); + + support.doContinue(); + + support.waitState (JPDADebugger.STATE_STOPPED); + + assertEquals ( + "Debugger stopped at wrong line for breakpoint", + lb[1].getLineNumber (), + debugger.getCurrentCallStackFrame ().getLineNumber (null) + ); + + for (int i = 0; i < tb.length; i++) { + tb[i].checkResult (); + } + for (int i = 0; i < tb.length; i++) { + dm.removeBreakpoint (lb[i]); + } + + support.doContinue (); + support.waitState (JPDADebugger.STATE_DISCONNECTED); + } finally { + if (support != null) support.doFinish (); + } + } + + public void testMultiLineLambdaBreakpoints() throws Exception { + try { + Utils.BreakPositions bp = Utils.getBreakPositions(TEST_MULTI_LINE_APP_PATH); + LineBreakpoint[] lb = bp.getBreakpoints().toArray(new LineBreakpoint[0]); + + lb[0].setLambdaIndex(new int[] {LineBreakpoint.LAMBDA_INDEX_STOP_OUTSIDE, 0}); //stop outside lambda + + DebuggerManager dm = DebuggerManager.getDebuggerManager (); + for (int i = 0; i < lb.length; i++) { + dm.addBreakpoint (lb[i]); + } + + TestBreakpointListener[] tb = new TestBreakpointListener[lb.length]; + for (int i = 0; i < lb.length; i++) { + tb[i] = new TestBreakpointListener (lb[i]); + lb[i].addJPDABreakpointListener (tb[i]); + } + support = JPDASupport.attach ( + "org.netbeans.api.debugger.jpda.testapps.ExpressionLambdaBreakpointMultiLineApp" + ); + + JPDADebugger debugger = support.getDebugger(); + + support.waitState (JPDADebugger.STATE_STOPPED); + assertEquals ( + "Debugger stopped at wrong line for breakpoint", + lb[0].getLineNumber (), debugger.getCurrentCallStackFrame ().getLineNumber (null) ); @@ -214,12 +274,27 @@ public void testLambdaBreakpointsStopOutsideOfLambda() throws Exception { assertEquals(String.valueOf(variablesByName), Set.of("args"), variablesByName.keySet()); support.doContinue(); - + + for (int i = 0; i < 5; i++) { + support.waitState (JPDADebugger.STATE_STOPPED); + assertEquals ( + "Debugger stopped at wrong line for breakpoint", + lb[0].getLineNumber (), + debugger.getCurrentCallStackFrame ().getLineNumber (null) + ); + + Map lambdaVariablesByName = getVariablesByName(debugger.getCurrentCallStackFrame ().getLocalVariables()); + + assertEquals(String.valueOf(lambdaVariablesByName), Set.of("l1"), lambdaVariablesByName.keySet()); + + support.doContinue(); + } + support.waitState (JPDADebugger.STATE_STOPPED); - + assertEquals ( - "Debugger stopped at wrong line for breakpoint", - lb[1].getLineNumber (), + "Debugger stopped at wrong line for breakpoint", + lb[1].getLineNumber (), debugger.getCurrentCallStackFrame ().getLineNumber (null) ); @@ -229,7 +304,7 @@ public void testLambdaBreakpointsStopOutsideOfLambda() throws Exception { for (int i = 0; i < tb.length; i++) { dm.removeBreakpoint (lb[i]); } - + support.doContinue (); support.waitState (JPDADebugger.STATE_DISCONNECTED); } finally { @@ -245,7 +320,7 @@ private static Map getVariablesByName(LocalVariable[] vars) { } return map; } - + private boolean checkMirrorValues(Map mirrorValues, Object[] actualValues){ String variableNameKey = "nonEmptyListCollection"; if(mirrorValues.containsKey(variableNameKey)){ @@ -269,7 +344,7 @@ public TestBreakpointListener (LineBreakpoint lineBreakpoint) { } public TestBreakpointListener ( - LineBreakpoint lineBreakpoint, + LineBreakpoint lineBreakpoint, int conditionResult ) { this.lineBreakpoint = lineBreakpoint; @@ -290,21 +365,21 @@ public void breakpointReached (JPDABreakpointEvent event) { private void checkEvent (JPDABreakpointEvent event) { this.event = event; assertEquals ( - "Breakpoint event: Wrong source breakpoint", - lineBreakpoint, + "Breakpoint event: Wrong source breakpoint", + lineBreakpoint, event.getSource () ); assertNotNull ( - "Breakpoint event: Context thread is null", + "Breakpoint event: Context thread is null", event.getThread () ); int result = event.getConditionResult (); - if ( result == JPDABreakpointEvent.CONDITION_FAILED && + if ( result == JPDABreakpointEvent.CONDITION_FAILED && conditionResult != JPDABreakpointEvent.CONDITION_FAILED ) failure = new AssertionError (event.getConditionException ()); - else + else if (result != conditionResult) failure = new AssertionError ( "Unexpected breakpoint condition result: " + result @@ -325,7 +400,7 @@ public void checkResult () { } if (failure != null) throw failure; } - + } - + } diff --git a/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/testapps/ExpressionLambdaBreakpointMultiLineApp.java b/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/testapps/ExpressionLambdaBreakpointMultiLineApp.java new file mode 100644 index 000000000000..b8940493566e --- /dev/null +++ b/java/debugger.jpda/test/unit/src/org/netbeans/api/debugger/jpda/testapps/ExpressionLambdaBreakpointMultiLineApp.java @@ -0,0 +1,38 @@ +/* + * 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.api.debugger.jpda.testapps; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Sample lambda expression breakpoints application. + * @author aksinsin + */ +public class ExpressionLambdaBreakpointMultiLineApp { + + public static void main(String... args) { + + List nonEmptyListCollection = Arrays.stream(new String[]{"a", "", "b", "", "c"}) + .map(l1 -> l1).map(l2 -> l2).map(l3 -> l3) // LBREAKPOINT + .collect(Collectors.toList()); + System.out.println(nonEmptyListCollection); // LBREAKPOINT + } +} diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpoint.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpoint.java index 93c753a5864c..537527f68454 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpoint.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpoint.java @@ -35,8 +35,6 @@ import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.debug.BreakpointEventArguments; -import org.eclipse.lsp4j.debug.BreakpointLocation; -import org.eclipse.lsp4j.debug.BreakpointLocationsResponse; import org.eclipse.lsp4j.debug.Source; import org.netbeans.api.debugger.Breakpoint; @@ -49,7 +47,6 @@ import org.netbeans.modules.java.lsp.server.debugging.DebugAdapterContext; import org.openide.filesystems.FileObject; import org.openide.filesystems.URLMapper; -import org.openide.util.Exceptions; /** * @@ -192,12 +189,11 @@ public CompletableFuture install() { return CompletableFuture.completedFuture(this); } - private int getLambdaIndex(int line, int column) { - //TODO: performance!! + private int[] getLambdaIndex(int line, int column) { try { FileObject file = Utils.fromUri(sourceURL); JavaSource js = JavaSource.forFileObject(file); - int[] index = new int[] { -1 }; + int[] index = new int[] { LineBreakpoint.LAMBDA_INDEX_STOP_OUTSIDE }; js.runUserActionTask(cc -> { cc.toPhase(JavaSource.Phase.PARSED); //TODO: span only! @@ -217,12 +213,12 @@ public Void visitLambdaExpression(LambdaExpressionTree tree, Void v) { } }.scan(cc.getCompilationUnit(), null); }, true); - return index[0]; + return index; } catch (IOException ex) { ex.printStackTrace(); } - return Integer.MIN_VALUE; + return new int[0]; } private static final String lsp2NBLogMessage(String message) { return message.replaceAll("\\{([^\\}]+)\\}", "{=$1}"); // NOI18N From 709a3eb6535b3c46851224c28a7dd1e6a6e95591 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 23 Apr 2026 15:54:40 +0200 Subject: [PATCH 4/4] Improving the handling of the lambda breakpoints, trying to add tests. --- java/java.lsp.server/nbproject/project.xml | 28 +- .../java/lsp/server/LspArgsProcessor.java | 2 +- .../server/debugging/DebugAdapterContext.java | 23 + .../server/debugging/NbProtocolServer.java | 7 +- .../NbBreakpointsRequestHandler.java | 21 +- .../debugging/launch/NbLaunchDelegate.java | 6 +- .../server/debugging/DebuggerServerTest.java | 489 ++++++++++++++++++ 7 files changed, 559 insertions(+), 17 deletions(-) create mode 100644 java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/debugging/DebuggerServerTest.java diff --git a/java/java.lsp.server/nbproject/project.xml b/java/java.lsp.server/nbproject/project.xml index 5854c2a36cdd..081ed1093a56 100644 --- a/java/java.lsp.server/nbproject/project.xml +++ b/java/java.lsp.server/nbproject/project.xml @@ -524,7 +524,7 @@ - 3 + 3 3.0 @@ -761,6 +761,10 @@ org.netbeans.modules.debugger.jpda.projects + + org.netbeans.modules.debugger.jpda.projectsui + + org.netbeans.modules.editor @@ -768,6 +772,11 @@ org.netbeans.modules.editor.actions + + org.netbeans.modules.editor.mimelookup + + + org.netbeans.modules.editor.mimelookup.impl @@ -779,6 +788,10 @@ org.netbeans.modules.extexecution.process + + org.netbeans.modules.gradle.java + + org.netbeans.modules.java.hints.declarative @@ -816,6 +829,11 @@ + + org.netbeans.modules.parsing.indexing + + + org.netbeans.modules.parsing.nb @@ -838,16 +856,12 @@ - org.openide.util.lookup + org.openide.modules - org.netbeans.modules.gradle.java - - - - org.openide.modules + org.openide.util.lookup diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspArgsProcessor.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspArgsProcessor.java index 1aea5aa7b913..05304cf11af7 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspArgsProcessor.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspArgsProcessor.java @@ -37,7 +37,7 @@ public final class LspArgsProcessor implements ArgsProcessor { @Messages("DESC_StartJavaLanguageServer=Starts the Java Language Server") public String lsPort; - @Arg(longName="start-java-debug-adapter-server") + @Arg(longName="start-java-debug-adapter-server", defaultValue = "") @Description(shortDescription="#DESC_StartJavaDebugAdapterServer") @Messages("DESC_StartJavaDebugAdapterServer=Starts the Java Debug Adapter Server") public String debugPort; diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/DebugAdapterContext.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/DebugAdapterContext.java index cfb099c74498..b872340355bb 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/DebugAdapterContext.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/DebugAdapterContext.java @@ -49,6 +49,7 @@ public final class DebugAdapterContext { private boolean clientLinesStartAt1 = true; private boolean clientColumnsStartAt1 = true; private final boolean debuggerLinesStartAt1 = true; + private final boolean debuggerColumnsStartAt1 = true; private boolean clientPathsAreUri = false; private final boolean debuggerPathsAreUri = true; private boolean supportsRunInTerminalRequest = false; @@ -127,6 +128,28 @@ public int getDebuggerLine(int clientLine) { } } + public int getClientColumn(int debuggerColum) { + if (clientColumnsStartAt1 == debuggerColumnsStartAt1) { + return debuggerColum; + } + if (clientColumnsStartAt1) { + return debuggerColum + 1; + } else { + return debuggerColum - 1; + } + } + + public int getDebuggerColumn(int clientColumn) { + if (clientColumnsStartAt1 == debuggerColumnsStartAt1) { + return clientColumn; + } + if (debuggerColumnsStartAt1) { + return clientColumn + 1; + } else { + return clientColumn - 1; + } + } + void setClientPathsAreUri(boolean clientPathsAreUri) { this.clientPathsAreUri = clientPathsAreUri; } diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java index 36b3366e97c5..079f41338c80 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java @@ -457,6 +457,7 @@ public CompletableFuture scopes(ScopesArguments args) { scope.setName(localScope.getName()); scope.setVariablesReference(localScopeId); scope.setExpensive(false); + scope.setPresentationHint("locals"); result.add(scope); } ScopesResponse response = new ScopesResponse(); @@ -683,15 +684,15 @@ public Void visitLambdaExpression(LambdaExpressionTree tree, Void v) { if (seenCodeOnLine.contains(line) && !addedLine.contains(line)) { BreakpointLocation l = new BreakpointLocation(); - l.setLine(line + 1); //XXX: +1 may need to be configuration + l.setLine(context.getClientLine(line + 1)); l.setColumn(null); locations.add(l); addedLine.add(line); } BreakpointLocation l = new BreakpointLocation(); - l.setLine(line + 1); //XXX: +1 may need to be configuration - l.setColumn(pos.getCharacter() + 1); //dtto + l.setLine(context.getClientLine(line + 1)); + l.setColumn(context.getClientColumn(pos.getCharacter() + 1)); locations.add(l); boolean wasInLambda = inLambda; diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpointsRequestHandler.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpointsRequestHandler.java index 62cb3bbd88b9..311baebb3597 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpointsRequestHandler.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpointsRequestHandler.java @@ -22,7 +22,9 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.apache.commons.io.FilenameUtils; @@ -133,23 +135,34 @@ public CompletableFuture setExceptionBreakpoint } private NbBreakpoint[] convertClientBreakpointsToDebugger(Source source, String sourceFile, SourceBreakpoint[] sourceBreakpoints, DebugAdapterContext context) { + Set linesWithSpecificColumns = new HashSet<>(); int n = sourceBreakpoints.length; int[] lines = new int[n]; Integer[] columns = new Integer[n]; for (int i = 0; i < n; i++) { lines[i] = context.getDebuggerLine(sourceBreakpoints[i].getLine()); - columns[i] = sourceBreakpoints[i].getColumn() != null ? context.getDebuggerLine(sourceBreakpoints[i].getColumn()) : null; + columns[i] = sourceBreakpoints[i].getColumn() != null ? context.getDebuggerColumn(sourceBreakpoints[i].getColumn()) : null; + if (sourceBreakpoints[i].getColumn() != null) { + linesWithSpecificColumns.add(sourceBreakpoints[i].getLine()); + } } - NbBreakpoint[] breakpoints = new NbBreakpoint[n]; + List breakpoints = new ArrayList<>(); for (int i = 0; i < n; i++) { + String condition = sourceBreakpoints[i].getCondition(); + if (linesWithSpecificColumns.contains(sourceBreakpoints[i].getLine()) && + sourceBreakpoints[i].getColumn() == null) { + //if there's a breakpoint on a specific column, ignore the "whole line" breakpoint: + condition = "false"; + } + int hitCount = 0; try { hitCount = Integer.parseInt(sourceBreakpoints[i].getHitCondition()); } catch (NumberFormatException e) { hitCount = 0; // If hitCount is not a number, ignore the hitCount. } - breakpoints[i] = new NbBreakpoint(source, sourceFile, lines[i], columns[i], hitCount, sourceBreakpoints[i].getCondition(), sourceBreakpoints[i].getLogMessage(), context); + breakpoints.add(new NbBreakpoint(source, sourceFile, lines[i], columns[i], hitCount, condition, sourceBreakpoints[i].getLogMessage(), context)); } - return breakpoints; + return breakpoints.toArray(NbBreakpoint[]::new); } } diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java index 93cbc81339f0..2ea2750b1d4e 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -223,7 +224,7 @@ public void progressHandleCreated(ProgressOperationEvent e) { } } Object contextObject = (singleFile) ? toRun : prj; - TestProgressHandler testProgressHandler = ctx.getClient().getNbCodeCapabilities().hasTestResultsSupport() ? new TestProgressHandler(ctx.getClient(), context.getClient(), Utils.toUri(toRun)) : null; + TestProgressHandler testProgressHandler = ctx.getClient() != null && ctx.getClient().getNbCodeCapabilities().hasTestResultsSupport() ? new TestProgressHandler(ctx.getClient(), context.getClient(), Utils.toUri(toRun)) : null; Lookup launchCtx = new ProxyLookup( testProgressHandler != null ? Lookups.fixed(contextObject, ioContext, progress, testProgressHandler) : Lookups.fixed(contextObject, ioContext, progress), Lookup.getDefault() @@ -554,7 +555,8 @@ public void finished(boolean success) { Collection providers = findActionProviders(proj); for (ActionProvider ap : providers) { - if (ap.isActionEnabled(ActionProvider.COMMAND_BUILD, launchCtx)) { + if (supportsAction(ap, ActionProvider.COMMAND_BUILD) && + ap.isActionEnabled(ActionProvider.COMMAND_BUILD, launchCtx)) { Lookups.executeWith(launchCtx, () -> { ap.invokeAction(ActionProvider.COMMAND_BUILD, launchCtx); }); diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/debugging/DebuggerServerTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/debugging/DebuggerServerTest.java new file mode 100644 index 000000000000..788a57a299fe --- /dev/null +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/debugging/DebuggerServerTest.java @@ -0,0 +1,489 @@ +/* + * 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.java.lsp.server.debugging; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import org.eclipse.lsp4j.debug.BreakpointEventArguments; +import org.eclipse.lsp4j.debug.BreakpointLocation; +import org.eclipse.lsp4j.debug.BreakpointLocationsArguments; +import org.eclipse.lsp4j.debug.Capabilities; +import org.eclipse.lsp4j.debug.ConfigurationDoneArguments; +import org.eclipse.lsp4j.debug.ContinueArguments; +import org.eclipse.lsp4j.debug.ContinuedEventArguments; +import org.eclipse.lsp4j.debug.DisconnectArguments; +import org.eclipse.lsp4j.debug.ExitedEventArguments; +import org.eclipse.lsp4j.debug.InitializeRequestArguments; +import org.eclipse.lsp4j.debug.Scope; +import org.eclipse.lsp4j.debug.ScopesArguments; +import org.eclipse.lsp4j.debug.ScopesResponse; +import org.eclipse.lsp4j.debug.SetBreakpointsArguments; +import org.eclipse.lsp4j.debug.Source; +import org.eclipse.lsp4j.debug.SourceBreakpoint; +import org.eclipse.lsp4j.debug.StackFrameFormat; +import org.eclipse.lsp4j.debug.StackTraceArguments; +import org.eclipse.lsp4j.debug.StackTraceResponse; +import org.eclipse.lsp4j.debug.StoppedEventArguments; +import org.eclipse.lsp4j.debug.TerminateArguments; +import org.eclipse.lsp4j.debug.TerminatedEventArguments; +import org.eclipse.lsp4j.debug.ThreadEventArguments; +import org.eclipse.lsp4j.debug.Variable; +import org.eclipse.lsp4j.debug.VariablesArguments; +import org.eclipse.lsp4j.debug.VariablesResponse; +import org.eclipse.lsp4j.debug.launch.DSPLauncher; +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.SourceUtilsTestUtil; +import org.netbeans.api.java.source.SourceUtilsTestUtil2; +import org.netbeans.api.project.ui.OpenProjects; +import org.netbeans.api.sendopts.CommandLine; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.file.launcher.queries.MultiSourceRootProvider; +import org.netbeans.modules.java.lsp.server.LspArgsProcessor; +import org.netbeans.modules.java.source.parsing.ParameterNameProviderImpl; +import org.netbeans.modules.parsing.impl.indexing.implspi.CacheFolderProvider; +import org.netbeans.spi.sendopts.ArgsProcessor; +import org.openide.filesystems.Repository; +import org.openide.modules.ModuleInfo; +import org.openide.modules.Places; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.Pair; +import org.openide.util.Utilities; +import org.openide.util.lookup.ServiceProvider; +import org.openide.util.test.MockLookup; + +public class DebuggerServerTest extends NbTestCase { + + private static final long TIMEOUT = 10_000; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private Socket clientSocket; + private Thread serverThread; + + public DebuggerServerTest(String name) { + super(name); + } + + public void testBreakpoints() throws Exception { + File src = new File(getWorkDir(), "Test.java"); + src.getParentFile().mkdirs(); + String code = + """ + public class Test { + public static void main(String... args) { + System.err.println("start"); + + for (int i = 0; i < 5; i++) { + System.err.println(i); + } + } + } + """; + try (Writer w = new FileWriter(src)) { + w.write(code); + } + IDebugProtocolServer[] server = new IDebugProtocolServer[1]; + List> called = new ArrayList<>(); + IDebugProtocolClient client = new TestDebugProtocolClient(called); + Launcher serverLauncher = + DSPLauncher.createClientLauncher(client, clientSocket.getInputStream(), clientSocket.getOutputStream(), false, new PrintWriter(System.err)); + serverLauncher.startListening(); + server[0] = serverLauncher.getRemoteProxy(); + InitializeRequestArguments init = new InitializeRequestArguments(); + init.setAdapterID("test"); + init.setColumnsStartAt1(true); + init.setLinesStartAt1(true); + Capabilities capa = server[0].initialize(init).get(); + server[0].launch(Map.of("file", src.toString(), + "classPaths", List.of("whatever"))).get(); + awaitCallBack(called, "initialized"); + SetBreakpointsArguments setBreakpointsArguments = new SetBreakpointsArguments(); + Source source = new Source(); + + source.setPath(src.getAbsolutePath()); + setBreakpointsArguments.setSource(source); + setBreakpointsArguments.setBreakpoints(new SourceBreakpoint[] { + createSourceBreakpoint(6, null, null) + }); + server[0].setBreakpoints(setBreakpointsArguments).get(); + server[0].configurationDone(new ConfigurationDoneArguments()).get(); + for (int i = 0; i < 5; i++) { + assertEquals("breakpoint", awaitCallBack(called, "stopped")); + server[0].continue_(new ContinueArguments()).get(); + } + + assertEquals("exited:true", awaitCallBack(called, "thread")); + server[0].disconnect(new DisconnectArguments()).get(); + } + + public void testLambdaBreakpoints1() throws Exception { + doTestLambdaBreakpoints( + new LambdaBreakpointTestCase(List.of( + createSourceBreakpoint(9, null, null) + ), + List.of( + List.of("Static:", "input:0"), + List.of("Static:", "v:\"a\""), + List.of("Static:", "v:\"b\"") + ))); + } + + public void testLambdaBreakpoints2() throws Exception { + doTestLambdaBreakpoints( + new LambdaBreakpointTestCase(List.of( + createSourceBreakpoint(9, 18, null) + ), + List.of( + List.of("Static:", "v:\"a\""), + List.of("Static:", "v:\"b\"") + ))); + } + + public void testLambdaBreakpoints3() throws Exception { + doTestLambdaBreakpoints( + new LambdaBreakpointTestCase(List.of( + createSourceBreakpoint(9, null, null), + createSourceBreakpoint(9, 18, null) + ), + List.of( + List.of("Static:", "v:\"a\""), + List.of("Static:", "v:\"b\"") + ))); + } + + public void testLambdaBreakpoints4() throws Exception { + doTestLambdaBreakpoints( + new LambdaBreakpointTestCase(List.of( + createSourceBreakpoint(9, null, null), + createSourceBreakpoint(9, 1, null), + createSourceBreakpoint(9, 18, null) + ), + List.of( + List.of("Static:", "input:0"), + List.of("Static:", "v:\"a\""), + List.of("Static:", "v:\"b\"") + ))); + } + + public void testLambdaBreakpointsZeroBased1() throws Exception { + doTestLambdaBreakpoints( + new LambdaBreakpointTestCase(List.of( + createSourceBreakpoint(8, 17, null) + ), + List.of( + List.of("Static:", "v:\"a\""), + List.of("Static:", "v:\"b\"") + )), + false, + false); + } + + public void testLambdaBreakpointsZeroBased2() throws Exception { + doTestLambdaBreakpoints( + new LambdaBreakpointTestCase(List.of( + createSourceBreakpoint(9, 17, null) + ), + List.of( + List.of("Static:", "v:\"a\""), + List.of("Static:", "v:\"b\"") + )), + true, + false); + } + + public void testLambdaBreakpointsZeroBased3() throws Exception { + doTestLambdaBreakpoints( + new LambdaBreakpointTestCase(List.of( + createSourceBreakpoint(8, 18, null) + ), + List.of( + List.of("Static:", "v:\"a\""), + List.of("Static:", "v:\"b\"") + )), + false, + true); + } + + private record LambdaBreakpointTestCase(List breakpoints, List> variables) {} + + private void doTestLambdaBreakpoints(LambdaBreakpointTestCase testCase) throws Exception { + doTestLambdaBreakpoints(testCase, true, true); + } + + private void doTestLambdaBreakpoints(LambdaBreakpointTestCase testCase, boolean linesStartAt1, boolean columnsStartAt1) throws Exception { + File src = new File(getWorkDir(), "Test.java"); + src.getParentFile().mkdirs(); + String code = + """ + import java.util.List; + public class Test { + public static void main(String... args) { + helper(args.length); + } + private static void helper(int input) { + List.of("a", "b", "c") + .stream() + .map(v -> v.length()) + .max((v1, v2) -> v1 - v2); + } + } + """; + try (Writer w = new FileWriter(src)) { + w.write(code); + } + IDebugProtocolServer[] server = new IDebugProtocolServer[1]; + List> called = new ArrayList<>(); + TestDebugProtocolClient client = new TestDebugProtocolClient(called); + Launcher serverLauncher = + DSPLauncher.createClientLauncher(client, clientSocket.getInputStream(), clientSocket.getOutputStream(), false, new PrintWriter(System.err)); + serverLauncher.startListening(); + server[0] = serverLauncher.getRemoteProxy(); + InitializeRequestArguments init = new InitializeRequestArguments(); + init.setAdapterID("test"); + init.setColumnsStartAt1(columnsStartAt1); + init.setLinesStartAt1(linesStartAt1); + Capabilities capa = server[0].initialize(init).get(); + server[0].launch(Map.of("file", src.toString(), + "classPaths", List.of("whatever"))).get(); + awaitCallBack(called, "initialized"); + SetBreakpointsArguments setBreakpointsArguments = new SetBreakpointsArguments(); + Source source = new Source(); + + source.setPath(src.getAbsolutePath()); + setBreakpointsArguments.setSource(source); + setBreakpointsArguments.setBreakpoints(new SourceBreakpoint[] { + createSourceBreakpoint(3 + (linesStartAt1 ? 1 : 0), null, null) + }); + setBreakpointsArguments.setSourceModified(true); + server[0].setBreakpoints(setBreakpointsArguments).get(); + server[0].configurationDone(new ConfigurationDoneArguments()).get(); + assertEquals("breakpoint", awaitCallBack(called, "stopped")); + BreakpointLocationsArguments locationArgs = new BreakpointLocationsArguments(); + locationArgs.setSource(source); + locationArgs.setLine(4); + locationArgs.setColumn(0); + locationArgs.setEndLine(7); + locationArgs.setEndColumn(38); + BreakpointLocation[] locations = server[0].breakpointLocations(locationArgs).get().getBreakpoints(); + String lambdaBreakpointLocations = """ + [ + { + "line": 9 + }, + { + "line": 9, + "column": 18 + }, + { + "line": 10 + }, + { + "line": 10, + "column": 18 + } + ]"""; + if (!linesStartAt1) { + lambdaBreakpointLocations = + lambdaBreakpointLocations.replace("9", "8") + .replace("10", "9"); + } + if (!columnsStartAt1) { + lambdaBreakpointLocations = lambdaBreakpointLocations.replace("18", "17"); + } + assertEquals(lambdaBreakpointLocations, + GSON.toJson(locations)); + + setBreakpointsArguments.setBreakpoints(testCase.breakpoints.toArray(SourceBreakpoint[]::new)); + server[0].setBreakpoints(setBreakpointsArguments).get(); + for (List expectedVariables : testCase.variables()) { + server[0].continue_(new ContinueArguments()); + assertEquals("breakpoint", awaitCallBack(called, "stopped")); + List actualVariables = getVariables(server[0], client.mainThreadID); + assertEquals(expectedVariables, actualVariables); + } + server[0].disconnect(new DisconnectArguments()).get(); + } + + private static SourceBreakpoint createSourceBreakpoint(int line, Integer column, String condition) { + SourceBreakpoint result = new SourceBreakpoint(); + + result.setLine(line); + result.setColumn(column); + result.setCondition(condition); + + return result; + } + + private static List getVariables(IDebugProtocolServer server, int threadId) throws Exception { + StackTraceArguments stackTraceRequest = new StackTraceArguments(); + stackTraceRequest.setThreadId(threadId); + StackTraceResponse stackTrace = server.stackTrace(stackTraceRequest).get(); + ScopesArguments scopesRequest = new ScopesArguments(); + scopesRequest.setFrameId(stackTrace.getStackFrames()[0].getId()); + ScopesResponse scopes = server.scopes(scopesRequest).get(); + Scope localScope = Arrays.stream(scopes.getScopes()) + .filter(scope -> "locals".equals(scope.getPresentationHint())) + .findAny() + .orElseThrow(); + VariablesArguments variablesRequest = new VariablesArguments(); + variablesRequest.setVariablesReference(localScope.getVariablesReference()); + VariablesResponse variables = server.variables(variablesRequest).get(); + List result = new ArrayList<>(); + for (Variable v : variables.getVariables()) { + result.add(v.getName() + ":" + v.getValue()); + } + return result; + } + + private Object awaitCallBack(List> called, String callback) { + long start = System.currentTimeMillis(); + synchronized (called) { + while ((System.currentTimeMillis() - start) < TIMEOUT) { + for (int i = 0; i < called.size(); i++) { + if (callback.equals(called.get(i).first())) { + Object res = called.get(i).second(); + while (i >= 0) { + called.remove(0); + i--; + } + return res; + } + } + try { + called.wait(TIMEOUT - (System.currentTimeMillis() - start)); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + } + + throw new AssertionError(String.valueOf(called)); + } + + @Override + protected void setUp() throws Exception { + System.setProperty("java.awt.headless", Boolean.TRUE.toString()); + ParameterNameProviderImpl.DISABLE_PARAMETER_NAMES_LOADING = true; + super.setUp(); + clearWorkDir(); + ServerSocket srv = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); + serverThread = new Thread(() -> { + try { + Socket server = srv.accept(); + + Path tempDir = Files.createTempDirectory("lsp-server"); + File userdir = tempDir.resolve("scratch-user").toFile(); + File cachedir = tempDir.resolve("scratch-cache").toFile(); + System.setProperty("netbeans.user", userdir.getAbsolutePath()); + File varLog = new File(new File(userdir, "var"), "log"); + varLog.mkdirs(); + System.setProperty("jdk.home", System.getProperty("java.home")); //for j2seplatform + Class main = Class.forName("org.netbeans.core.startup.Main"); + main.getDeclaredMethod("initializeURLFactory").invoke(null); + new File(cachedir, "index").mkdirs(); + Class jsClass = JavaSource.class; + File javaCluster = Utilities.toFile(jsClass.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile().getParentFile(); + System.setProperty("netbeans.dirs", javaCluster.getAbsolutePath()); + CacheFolderProvider.getCacheFolderForRoot(Utilities.toURI(Places.getUserDirectory()).toURL(), EnumSet.noneOf(CacheFolderProvider.Kind.class), CacheFolderProvider.Mode.EXISTENT); + + Lookup.getDefault().lookup(ModuleInfo.class); //start the module system + + CommandLine.getDefault().process(new String[] {"--start-java-debug-adapter-server"}, server.getInputStream(), server.getOutputStream(), System.err, getWorkDir()); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + }); + serverThread.start(); + clientSocket = new Socket(srv.getInetAddress(), srv.getLocalPort()); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + try { + serverThread.stop(); + } catch (UnsupportedOperationException ex) { + } + OpenProjects.getDefault().close(OpenProjects.getDefault().getOpenProjects()); + } + + private class TestDebugProtocolClient implements IDebugProtocolClient { + + private final List> called; + + public TestDebugProtocolClient(List> called) { + this.called = called; + } + private int mainThreadID = -1; + + @Override + public void initialized() { + recordState("initialized", null); + } + + @Override + public void stopped(StoppedEventArguments args) { + if (mainThreadID == (-1)) { + mainThreadID = args.getThreadId(); + } + recordState("stopped", args.getReason()); + } + + @Override + public void thread(ThreadEventArguments args) { + recordState("thread", args.getReason() + ":" + (mainThreadID == args.getThreadId())); + } + + @Override + public void exited(ExitedEventArguments args) { + recordState("exited", null); + } + + @Override + public void terminated(TerminatedEventArguments args) { + recordState("terminated", null); + } + + private void recordState(String event, Object data) { + synchronized (called) { + called.add(Pair.of(event, data)); + called.notifyAll(); + } + } + } +}