From 8191cf72342c6dac74294dbe7325856572d3e0cd Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Mon, 19 Sep 2022 14:39:38 +0800 Subject: [PATCH] Visualize the inline breakpoint locations --- .../microsoft/java/debug/core/Breakpoint.java | 110 ++++++++++------ .../java/debug/core/DebugSession.java | 5 + .../debug/core/EvaluatableBreakpoint.java | 8 +- .../java/debug/core/IBreakpoint.java | 6 +- .../java/debug/core/IDebugSession.java | 4 +- .../debug/core/JavaBreakpointLocation.java | 113 +++++++++++++++++ .../java/debug/core/adapter/AdapterUtils.java | 8 +- .../java/debug/core/adapter/DebugAdapter.java | 4 +- .../core/adapter/DebugAdapterContext.java | 6 + .../core/adapter/IDebugAdapterContext.java | 4 +- .../core/adapter/ISourceLookUpProvider.java | 22 +++- .../BreakpointLocationsRequestHander.java | 83 ++++++++++++ .../handler/InitializeRequestHandler.java | 3 +- .../handler/SetBreakpointsRequestHandler.java | 78 ++++++------ .../handler/StackTraceRequestHandler.java | 29 ++++- .../java/debug/core/protocol/Requests.java | 40 +++++- .../java/debug/core/protocol/Responses.java | 18 ++- .../java/debug/core/protocol/Types.java | 53 +++++++- .../java/debug/LambdaExpressionLocator.java | 11 +- .../internal/JdtSourceLookUpProvider.java | 120 +++++++++++++++--- 20 files changed, 605 insertions(+), 120 deletions(-) create mode 100644 com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/JavaBreakpointLocation.java create mode 100644 com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/BreakpointLocationsRequestHander.java diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java index 8b865d44f..dfdbad4bb 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java @@ -37,13 +37,11 @@ public class Breakpoint implements IBreakpoint { private VirtualMachine vm = null; private IEventHub eventHub = null; - private String className = null; - private int lineNumber = 0; + private JavaBreakpointLocation sourceLocation = null; private int hitCount = 0; private String condition = null; private String logMessage = null; private HashMap propertyMap = new HashMap<>(); - private String methodSignature = null; private boolean async = false; @@ -56,21 +54,37 @@ public class Breakpoint implements IBreakpoint { } Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, String condition) { + this(vm, eventHub, className, lineNumber, hitCount, condition, null); + } + + Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, String condition, String logMessage) { this.vm = vm; this.eventHub = eventHub; + String contextClass = className; + String methodName = null; + String methodSignature = null; if (className != null && className.contains("#")) { - this.className = className.substring(0, className.indexOf("#")); - this.methodSignature = className.substring(className.indexOf("#") + 1); - } else { - this.className = className; + contextClass = className.substring(0, className.indexOf("#")); + String[] methodInfo = className.substring(className.indexOf("#") + 1).split("#"); + methodName = methodInfo[0]; + methodSignature = methodInfo[1]; } - this.lineNumber = lineNumber; + + this.sourceLocation = new JavaBreakpointLocation(lineNumber, -1); + this.sourceLocation.setClassName(contextClass); + this.sourceLocation.setMethodName(methodName); + this.sourceLocation.setMethodSignature(methodSignature); this.hitCount = hitCount; this.condition = condition; + this.logMessage = logMessage; } - Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, String condition, String logMessage) { - this(vm, eventHub, className, lineNumber, hitCount, condition); + Breakpoint(VirtualMachine vm, IEventHub eventHub, JavaBreakpointLocation sourceLocation, int hitCount, String condition, String logMessage) { + this.vm = vm; + this.eventHub = eventHub; + this.sourceLocation = sourceLocation; + this.hitCount = hitCount; + this.condition = condition; this.logMessage = logMessage; } @@ -104,14 +118,24 @@ public void close() throws Exception { } // IBreakpoint + @Override + public JavaBreakpointLocation sourceLocation() { + return this.sourceLocation; + } + @Override public String className() { - return className; + return this.sourceLocation.className(); } @Override public int getLineNumber() { - return lineNumber; + return this.sourceLocation.lineNumber(); + } + + @Override + public int getColumnNumber() { + return this.sourceLocation.columnNumber(); } @Override @@ -120,20 +144,20 @@ public String getCondition() { } @Override - public boolean equals(Object obj) { - if (!(obj instanceof Breakpoint)) { - return super.equals(obj); - } - - Breakpoint breakpoint = (Breakpoint) obj; - return Objects.equals(this.className(), breakpoint.className()) - && this.getLineNumber() == breakpoint.getLineNumber() - && Objects.equals(this.methodSignature, breakpoint.methodSignature); + public int hashCode() { + return Objects.hash(sourceLocation); } @Override - public int hashCode() { - return Objects.hash(this.className, this.lineNumber, this.methodSignature); + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Breakpoint)) { + return false; + } + Breakpoint other = (Breakpoint) obj; + return Objects.equals(sourceLocation, other.sourceLocation); } @Override @@ -149,6 +173,7 @@ public void setHitCount(int hitCount) { .filter(request -> request instanceof BreakpointRequest) .subscribe(request -> { request.addCountFilter(hitCount); + request.disable(); request.enable(); }); } @@ -183,13 +208,13 @@ public CompletableFuture install() { // It's possible that different class loaders create new class with the same name. // Here to listen to future class prepare events to handle such case. ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest(); - classPrepareRequest.addClassFilter(className); + classPrepareRequest.addClassFilter(className()); classPrepareRequest.enable(); requests.add(classPrepareRequest); // Local types also needs to be handled ClassPrepareRequest localClassPrepareRequest = vm.eventRequestManager().createClassPrepareRequest(); - localClassPrepareRequest.addClassFilter(className + "$*"); + localClassPrepareRequest.addClassFilter(className() + "$*"); localClassPrepareRequest.enable(); requests.add(localClassPrepareRequest); @@ -202,7 +227,7 @@ public CompletableFuture install() { .subscribe(debugEvent -> { ClassPrepareEvent event = (ClassPrepareEvent) debugEvent.event; List newRequests = AsyncJdwpUtils.await( - createBreakpointRequests(event.referenceType(), lineNumber, hitCount, false) + createBreakpointRequests(event.referenceType(), getLineNumber(), hitCount, false) ); requests.addAll(newRequests); if (!newRequests.isEmpty() && !future.isDone()) { @@ -213,8 +238,8 @@ public CompletableFuture install() { subscriptions.add(subscription); Runnable resolveRequestsFromExistingClasses = () -> { - List refTypes = vm.classesByName(className); - createBreakpointRequests(refTypes, lineNumber, hitCount, true) + List refTypes = vm.classesByName(className()); + createBreakpointRequests(refTypes, getLineNumber(), hitCount, true) .whenComplete((newRequests, ex) -> { if (ex != null) { return; @@ -281,14 +306,13 @@ private CompletableFuture> collectLocations(ReferenceType refType }); } - private CompletableFuture> collectLocations(List refTypes, String nameAndSignature) { - String[] segments = nameAndSignature.split("#"); + private CompletableFuture> collectLocations(List refTypes, String methodName, String methodSiguature) { List> futures = new ArrayList<>(); for (ReferenceType refType : refTypes) { if (async()) { - futures.add(AsyncJdwpUtils.supplyAsync(() -> findMethodLocaiton(refType, segments[0], segments[1]))); + futures.add(AsyncJdwpUtils.supplyAsync(() -> findMethodLocaiton(refType, methodName, methodSiguature))); } else { - futures.add(CompletableFuture.completedFuture(findMethodLocaiton(refType, segments[0], segments[1]))); + futures.add(CompletableFuture.completedFuture(findMethodLocaiton(refType, methodName, methodSiguature))); } } @@ -329,10 +353,22 @@ private CompletableFuture> createBreakpointRequests(Refe private CompletableFuture> createBreakpointRequests(List refTypes, int lineNumber, int hitCount, boolean includeNestedTypes) { CompletableFuture> locationsFuture; - if (this.methodSignature != null) { - locationsFuture = collectLocations(refTypes, this.methodSignature); + if (this.sourceLocation.methodName() != null) { + locationsFuture = collectLocations(refTypes, this.sourceLocation.methodName(), this.sourceLocation.methodSignature()); } else { - locationsFuture = collectLocations(refTypes, lineNumber, includeNestedTypes); + locationsFuture = collectLocations(refTypes, lineNumber, includeNestedTypes).thenApply((locations) -> { + if (locations.isEmpty()) { + return locations; + } + + /** + * For a line breakpoint, we default to breaking at the first location + * of the line. If you want to break at other locations on the same line, + * you can add an inline breakpoint based on the locations returned by + * the BreakpointLocation request. + */ + return Arrays.asList(locations.get(0)); + }); } return locationsFuture.thenCompose((locations) -> { @@ -389,11 +425,11 @@ private CompletableFuture> createBreakpointRequests(List } private Object computeRequestType() { - if (this.methodSignature == null) { + if (this.sourceLocation.methodName() == null) { return IBreakpoint.REQUEST_TYPE_LINE; } - if (this.methodSignature.startsWith("lambda$")) { + if (this.sourceLocation.methodName().startsWith("lambda$")) { return IBreakpoint.REQUEST_TYPE_LAMBDA; } else { return IBreakpoint.REQUEST_TYPE_METHOD; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java index ed711bf82..1c7990f16 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java @@ -114,6 +114,11 @@ public void terminate() { } } + @Override + public IBreakpoint createBreakpoint(JavaBreakpointLocation sourceLocation, int hitCount, String condition, String logMessage) { + return new EvaluatableBreakpoint(vm, this.getEventHub(), sourceLocation, hitCount, condition, logMessage); + } + @Override public IBreakpoint createBreakpoint(String className, int lineNumber, int hitCount, String condition, String logMessage) { return new EvaluatableBreakpoint(vm, this.getEventHub(), className, lineNumber, hitCount, condition, logMessage); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/EvaluatableBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/EvaluatableBreakpoint.java index 9b3fdb2dd..723e2cadf 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/EvaluatableBreakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/EvaluatableBreakpoint.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2018 Microsoft Corporation and others. +* Copyright (c) 2018-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -48,6 +48,12 @@ public class EvaluatableBreakpoint extends Breakpoint implements IEvaluatableBre this.eventHub = eventHub; } + EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, JavaBreakpointLocation sourceLocation, int hitCount, + String condition, String logMessage) { + super(vm, eventHub, sourceLocation, hitCount, condition, logMessage); + this.eventHub = eventHub; + } + @Override public boolean containsEvaluatableExpression() { return containsConditionalExpression() || containsLogpointExpression(); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java index 04fbf5005..40995e9dd 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -23,10 +23,14 @@ public interface IBreakpoint extends IDebugResource { int REQUEST_TYPE_LAMBDA = 2; + JavaBreakpointLocation sourceLocation(); + String className(); int getLineNumber(); + int getColumnNumber(); + int getHitCount(); void setHitCount(int hitCount); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java index 03780c2d9..4e4078c87 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -30,6 +30,8 @@ public interface IDebugSession { // breakpoints IBreakpoint createBreakpoint(String className, int lineNumber, int hitCount, String condition, String logMessage); + IBreakpoint createBreakpoint(JavaBreakpointLocation sourceLocation, int hitCount, String condition, String logMessage); + IWatchpoint createWatchPoint(String className, String fieldName, String accessType, String condition, int hitCount); void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/JavaBreakpointLocation.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/JavaBreakpointLocation.java new file mode 100644 index 000000000..820e80c9b --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/JavaBreakpointLocation.java @@ -0,0 +1,113 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core; + +import java.util.Objects; + +import com.microsoft.java.debug.core.protocol.Types; + +public class JavaBreakpointLocation { + /** + * The source line of the breakpoint or logpoint. + */ + private int lineNumber; + /** + * The source column of the breakpoint. + */ + private int columnNumber = -1; + /** + * The declaring class name that encloses the target position. + */ + private String className; + /** + * The method name and signature when the target position + * points to a method declaration. + */ + private String methodName; + private String methodSignature; + /** + * All possible locations for source breakpoints in a given range. + */ + private Types.BreakpointLocation[] availableBreakpointLocations = new Types.BreakpointLocation[0]; + + public JavaBreakpointLocation(int lineNumber, int columnNumber) { + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } + + @Override + public int hashCode() { + return Objects.hash(lineNumber, columnNumber, className, methodName, methodSignature); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof JavaBreakpointLocation)) { + return false; + } + JavaBreakpointLocation other = (JavaBreakpointLocation) obj; + return lineNumber == other.lineNumber && columnNumber == other.columnNumber + && Objects.equals(className, other.className) && Objects.equals(methodName, other.methodName) + && Objects.equals(methodSignature, other.methodSignature); + } + + public int lineNumber() { + return lineNumber; + } + + public void setLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + } + + public int columnNumber() { + return columnNumber; + } + + public void setColumnNumber(int columnNumber) { + this.columnNumber = columnNumber; + } + + public String className() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String methodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public String methodSignature() { + return methodSignature; + } + + public void setMethodSignature(String methodSignature) { + this.methodSignature = methodSignature; + } + + public Types.BreakpointLocation[] availableBreakpointLocations() { + return availableBreakpointLocations; + } + + public void setAvailableBreakpointLocations(Types.BreakpointLocation[] availableBreakpointLocations) { + this.availableBreakpointLocations = availableBreakpointLocations; + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/AdapterUtils.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/AdapterUtils.java index 5c1272102..9b972334d 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/AdapterUtils.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/AdapterUtils.java @@ -119,13 +119,15 @@ public static int convertLineNumber(int line, boolean sourceLinesStartAt1, boole * the column number from the source platform * @param sourceColumnsStartAt1 * the source platform's column starts at 1 or not + * @param targetColumnStartAt1 + * the target platform's column starts at 1 or not * @return the new column number */ - public static int convertColumnNumber(int column, boolean sourceColumnsStartAt1) { + public static int convertColumnNumber(int column, boolean sourceColumnsStartAt1, boolean targetColumnStartAt1) { if (sourceColumnsStartAt1) { - return column - 1; + return targetColumnStartAt1 ? column : column - 1; } else { - return column; + return targetColumnStartAt1 ? column + 1 : column; } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java index 0bf5d2683..edc771163 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java @@ -21,6 +21,7 @@ import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.adapter.handler.AttachRequestHandler; +import com.microsoft.java.debug.core.adapter.handler.BreakpointLocationsRequestHander; import com.microsoft.java.debug.core.adapter.handler.CompletionsHandler; import com.microsoft.java.debug.core.adapter.handler.ConfigurationDoneRequestHandler; import com.microsoft.java.debug.core.adapter.handler.DataBreakpointInfoRequestHandler; @@ -105,7 +106,7 @@ private void initialize() { registerHandler(new InitializeRequestHandler()); registerHandler(new LaunchRequestHandler()); - // DEBUG node only + // DEBUG mode only registerHandlerForDebug(new AttachRequestHandler()); registerHandlerForDebug(new ConfigurationDoneRequestHandler()); registerHandlerForDebug(new DisconnectRequestHandler()); @@ -129,6 +130,7 @@ private void initialize() { registerHandlerForDebug(new RefreshVariablesHandler()); registerHandlerForDebug(new ProcessIdHandler()); registerHandlerForDebug(new SetFunctionBreakpointsRequestHandler()); + registerHandlerForDebug(new BreakpointLocationsRequestHander()); // NO_DEBUG mode only registerHandlerForNoDebug(new DisconnectRequestWithoutDebuggingHandler()); registerHandlerForNoDebug(new ProcessIdHandler()); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java index 4185d7597..e317ddf1c 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java @@ -38,6 +38,8 @@ public class DebugAdapterContext implements IDebugAdapterContext { private IDebugSession debugSession; private boolean debuggerLinesStartAt1 = true; + // The Java model on debugger uses 0-based column number. + private boolean debuggerColumnStartAt1 = false; private boolean debuggerPathsAreUri = true; private boolean clientLinesStartAt1 = true; private boolean clientColumnsStartAt1 = true; @@ -105,6 +107,10 @@ public void setDebuggerLinesStartAt1(boolean debuggerLinesStartAt1) { this.debuggerLinesStartAt1 = debuggerLinesStartAt1; } + public boolean isDebuggerColumnsStartAt1() { + return debuggerColumnStartAt1; + } + @Override public boolean isDebuggerPathsAreUri() { return debuggerPathsAreUri; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java index bfcad8905..5d3b75639 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -55,6 +55,8 @@ public interface IDebugAdapterContext { void setClientColumnsStartAt1(boolean clientColumnsStartAt1); + boolean isDebuggerColumnsStartAt1(); + boolean isClientPathsAreUri(); void setClientPathsAreUri(boolean clientPathsAreUri); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java index dd3e4dbde..9976fc16f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,12 +12,32 @@ package com.microsoft.java.debug.core.adapter; import com.microsoft.java.debug.core.DebugException; +import com.microsoft.java.debug.core.JavaBreakpointLocation; +import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint; public interface ISourceLookUpProvider extends IProvider { boolean supportsRealtimeBreakpointVerification(); + /** + * Deprecated, please use {@link #getBreakpointLocations(String, SourceBreakpoint[])} instead. + */ + @Deprecated String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) throws DebugException; + /** + * Given a set of source breakpoint locations with line and column numbers, + * verify if they are valid breakpoint locations. If it's a valid location, + * resolve its enclosing class name, method name and signature (for method + * breakpoint) and all possible inline breakpoint locations in that line. + * + * @param sourceUri + * the source file uri + * @param sourceBreakpoints + * the source breakpoints with line and column numbers + * @return Locations of Breakpoints containing context class and method information. + */ + JavaBreakpointLocation[] getBreakpointLocations(String sourceUri, SourceBreakpoint[] sourceBreakpoints) throws DebugException; + /** * Given a fully qualified class name and source file path, search the associated disk source file. * diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/BreakpointLocationsRequestHander.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/BreakpointLocationsRequestHander.java new file mode 100644 index 000000000..57c7ea15e --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/BreakpointLocationsRequestHander.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2022 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.java.debug.core.adapter.handler; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.java.debug.core.IBreakpoint; +import com.microsoft.java.debug.core.adapter.AdapterUtils; +import com.microsoft.java.debug.core.adapter.ErrorCode; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +import com.microsoft.java.debug.core.protocol.Requests; +import com.microsoft.java.debug.core.protocol.Responses; +import com.microsoft.java.debug.core.protocol.Messages.Response; +import com.microsoft.java.debug.core.protocol.Requests.Arguments; +import com.microsoft.java.debug.core.protocol.Requests.BreakpointLocationsArguments; +import com.microsoft.java.debug.core.protocol.Requests.Command; +import com.microsoft.java.debug.core.protocol.Types.BreakpointLocation; + +/** + * The breakpointLocations request returns all possible locations for source breakpoints in a given range. + * Clients should only call this request if the corresponding capability supportsBreakpointLocationsRequest is true. + */ +public class BreakpointLocationsRequestHander implements IDebugRequestHandler { + + @Override + public List getTargetCommands() { + return Arrays.asList(Requests.Command.BREAKPOINTLOCATIONS); + } + + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + BreakpointLocationsArguments bpArgs = (BreakpointLocationsArguments) arguments; + String sourceUri = SetBreakpointsRequestHandler.normalizeSourcePath(bpArgs.source, context); + // When breakpoint source path is null or an invalid file path, send an ErrorResponse back. + if (StringUtils.isBlank(sourceUri)) { + throw AdapterUtils.createCompletionException( + String.format("Failed to get BreakpointLocations. Reason: '%s' is an invalid path.", bpArgs.source.path), + ErrorCode.SET_BREAKPOINT_FAILURE); + } + + int debuggerLine = AdapterUtils.convertLineNumber(bpArgs.line, context.isClientLinesStartAt1(), context.isDebuggerLinesStartAt1()); + IBreakpoint[] breakpoints = context.getBreakpointManager().getBreakpoints(sourceUri); + BreakpointLocation[] locations = new BreakpointLocation[0]; + for (int i = 0; i < breakpoints.length; i++) { + if (breakpoints[i].getLineNumber() == debuggerLine && ArrayUtils.isNotEmpty( + breakpoints[i].sourceLocation().availableBreakpointLocations())) { + locations = Stream.of(breakpoints[i].sourceLocation().availableBreakpointLocations()).map(location -> { + BreakpointLocation newLocaiton = new BreakpointLocation(); + newLocaiton.line = AdapterUtils.convertLineNumber(location.line, + context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + newLocaiton.column = AdapterUtils.convertColumnNumber(location.column, + context.isDebuggerColumnsStartAt1(), context.isClientColumnsStartAt1()); + newLocaiton.endLine = AdapterUtils.convertLineNumber(location.endLine, + context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + newLocaiton.endColumn = AdapterUtils.convertColumnNumber(location.endColumn, + context.isDebuggerColumnsStartAt1(), context.isClientColumnsStartAt1()); + return newLocaiton; + }).toArray(BreakpointLocation[]::new); + break; + } + } + + response.body = new Responses.BreakpointLocationsResponseBody(locations); + return CompletableFuture.completedFuture(response); + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java index ae92f356f..19bbd7d62 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -64,6 +64,7 @@ public CompletableFuture handle(Requests.Command command, Req caps.supportsDataBreakpoints = true; caps.supportsFunctionBreakpoints = true; caps.supportsClipboardContext = true; + caps.supportsBreakpointLocationsRequest = true; response.body = caps; return CompletableFuture.completedFuture(response); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java index d02f73bfd..7fe6c3fdd 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java @@ -17,6 +17,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; @@ -26,6 +27,7 @@ import com.microsoft.java.debug.core.IBreakpoint; import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.IEvaluatableBreakpoint; +import com.microsoft.java.debug.core.JavaBreakpointLocation; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.HotCodeReplaceEvent.EventType; @@ -93,28 +95,7 @@ public CompletableFuture handle(Command command, Arguments arguments, } SetBreakpointArguments bpArguments = (SetBreakpointArguments) arguments; - String clientPath = bpArguments.source.path; - if (AdapterUtils.isWindows()) { - // VSCode may send drive letters with inconsistent casing which will mess up the key - // in the BreakpointManager. See https://github.com/Microsoft/vscode/issues/6268 - // Normalize the drive letter casing. Note that drive letters - // are not localized so invariant is safe here. - String drivePrefix = FilenameUtils.getPrefix(clientPath); - if (drivePrefix != null && drivePrefix.length() >= 2 - && Character.isLowerCase(drivePrefix.charAt(0)) && drivePrefix.charAt(1) == ':') { - drivePrefix = drivePrefix.substring(0, 2); // d:\ is an illegal regex string, convert it to d: - clientPath = clientPath.replaceFirst(drivePrefix, drivePrefix.toUpperCase()); - } - } - String sourcePath = clientPath; - if (bpArguments.source.sourceReference != 0 && context.getSourceUri(bpArguments.source.sourceReference) != null) { - sourcePath = context.getSourceUri(bpArguments.source.sourceReference); - } else if (StringUtils.isNotBlank(clientPath)) { - // See the bug https://github.com/Microsoft/vscode/issues/30996 - // Source.path in the SetBreakpointArguments could be a file system path or uri. - sourcePath = AdapterUtils.convertPath(clientPath, AdapterUtils.isUri(clientPath), context.isDebuggerPathsAreUri()); - } - + String sourcePath = normalizeSourcePath(bpArguments.source, context); // When breakpoint source path is null or an invalid file path, send an ErrorResponse back. if (StringUtils.isBlank(sourcePath)) { throw AdapterUtils.createCompletionException( @@ -163,6 +144,32 @@ public CompletableFuture handle(Command command, Arguments arguments, } } + public static String normalizeSourcePath(Types.Source source, IDebugAdapterContext context) { + String clientPath = source.path; + if (AdapterUtils.isWindows()) { + // VSCode may send drive letters with inconsistent casing which will mess up the key + // in the BreakpointManager. See https://github.com/Microsoft/vscode/issues/6268 + // Normalize the drive letter casing. Note that drive letters + // are not localized so invariant is safe here. + String drivePrefix = FilenameUtils.getPrefix(clientPath); + if (drivePrefix != null && drivePrefix.length() >= 2 + && Character.isLowerCase(drivePrefix.charAt(0)) && drivePrefix.charAt(1) == ':') { + drivePrefix = drivePrefix.substring(0, 2); // d:\ is an illegal regex string, convert it to d: + clientPath = clientPath.replaceFirst(drivePrefix, drivePrefix.toUpperCase()); + } + } + String sourcePath = clientPath; + if (source.sourceReference != 0 && context.getSourceUri(source.sourceReference) != null) { + sourcePath = context.getSourceUri(source.sourceReference); + } else if (StringUtils.isNotBlank(clientPath)) { + // See the bug https://github.com/Microsoft/vscode/issues/30996 + // Source.path in the SetBreakpointArguments could be a file system path or uri. + sourcePath = AdapterUtils.convertPath(clientPath, AdapterUtils.isUri(clientPath), context.isDebuggerPathsAreUri()); + } + + return sourcePath; + } + private IBreakpoint getAssociatedEvaluatableBreakpoint(IDebugAdapterContext context, BreakpointEvent event) { return Arrays.asList(context.getBreakpointManager().getBreakpoints()).stream().filter( bp -> { @@ -173,11 +180,6 @@ private IBreakpoint getAssociatedEvaluatableBreakpoint(IDebugAdapterContext cont ).findFirst().orElse(null); } - private IBreakpoint getAssociatedBreakpoint(IDebugAdapterContext context, BreakpointEvent event) { - return Arrays.asList(context.getBreakpointManager().getBreakpoints()).stream() - .filter(bp -> bp.requests().contains(event.request())).findFirst().orElse(null); - } - private void registerBreakpointHandler(IDebugAdapterContext context) { IDebugSession debugSession = context.getDebugSession(); if (debugSession != null) { @@ -300,27 +302,25 @@ private Types.Breakpoint convertDebuggerBreakpointToClient(IBreakpoint breakpoin private IBreakpoint[] convertClientBreakpointsToDebugger(String sourceFile, Types.SourceBreakpoint[] sourceBreakpoints, IDebugAdapterContext context) throws DebugException { - int[] lines = Arrays.asList(sourceBreakpoints).stream().map(sourceBreakpoint -> { - return AdapterUtils.convertLineNumber(sourceBreakpoint.line, context.isClientLinesStartAt1(), context.isDebuggerLinesStartAt1()); - }).mapToInt(line -> line).toArray(); - - int[] columns = Arrays.asList(sourceBreakpoints).stream().map(b -> { - return AdapterUtils.convertColumnNumber(b.column, context.isClientColumnsStartAt1()); - }).mapToInt(b -> b).toArray(); + Types.SourceBreakpoint[] debugSourceBreakpoints = Stream.of(sourceBreakpoints).map(sourceBreakpoint -> { + int line = AdapterUtils.convertLineNumber(sourceBreakpoint.line, context.isClientLinesStartAt1(), context.isDebuggerLinesStartAt1()); + int column = AdapterUtils.convertColumnNumber(sourceBreakpoint.column, context.isClientColumnsStartAt1(), context.isDebuggerColumnsStartAt1()); + return new Types.SourceBreakpoint(line, column); + }).toArray(Types.SourceBreakpoint[]::new); ISourceLookUpProvider sourceProvider = context.getProvider(ISourceLookUpProvider.class); - String[] fqns = sourceProvider.getFullyQualifiedName(sourceFile, lines, columns); - IBreakpoint[] breakpoints = new IBreakpoint[lines.length]; - for (int i = 0; i < lines.length; i++) { + JavaBreakpointLocation[] locations = sourceProvider.getBreakpointLocations(sourceFile, debugSourceBreakpoints); + IBreakpoint[] breakpoints = new IBreakpoint[locations.length]; + for (int i = 0; i < locations.length; i++) { int hitCount = 0; try { hitCount = Integer.parseInt(sourceBreakpoints[i].hitCondition); } catch (NumberFormatException e) { hitCount = 0; // If hitCount is an illegal number, ignore hitCount condition. } - breakpoints[i] = context.getDebugSession().createBreakpoint(fqns[i], lines[i], hitCount, sourceBreakpoints[i].condition, + breakpoints[i] = context.getDebugSession().createBreakpoint(locations[i], hitCount, sourceBreakpoints[i].condition, sourceBreakpoints[i].logMessage); - if (sourceProvider.supportsRealtimeBreakpointVerification() && StringUtils.isNotBlank(fqns[i])) { + if (sourceProvider.supportsRealtimeBreakpointVerification() && StringUtils.isNotBlank(locations[i].className())) { breakpoints[i].putProperty("verified", true); } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java index 2120cafab..f22966b03 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -25,6 +26,7 @@ import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.DebugUtility; +import com.microsoft.java.debug.core.IBreakpoint; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; @@ -47,6 +49,7 @@ import com.sun.jdi.ReferenceType; import com.sun.jdi.StackFrame; import com.sun.jdi.ThreadReference; +import com.sun.jdi.request.BreakpointRequest; public class StackTraceRequestHandler implements IDebugRequestHandler { @@ -89,7 +92,7 @@ public CompletableFuture handle(Command command, Arguments arguments, StackFrameReference stackframe = new StackFrameReference(thread, stacktraceArgs.startFrame + i); int frameId = context.getRecyclableIdPool().addObject(stacktraceArgs.threadId, stackframe); StackFrameInfo jdiFrame = jdiFrames.get(i); - result.add(convertDebuggerStackFrameToClient(jdiFrame, frameId, context)); + result.add(convertDebuggerStackFrameToClient(jdiFrame, frameId, i == 0, context)); } } catch (IncompatibleThreadStateException | IndexOutOfBoundsException | URISyntaxException | AbsentInformationException | ObjectCollectedException @@ -167,7 +170,7 @@ private static List resolveStackFrameInfos(StackFrame[] frames, return jdiFrames; } - private Types.StackFrame convertDebuggerStackFrameToClient(StackFrameInfo jdiFrame, int frameId, IDebugAdapterContext context) + private Types.StackFrame convertDebuggerStackFrameToClient(StackFrameInfo jdiFrame, int frameId, boolean isTopFrame, IDebugAdapterContext context) throws URISyntaxException, AbsentInformationException { Types.Source clientSource = convertDebuggerSourceToClient(jdiFrame.typeName, jdiFrame.sourceName, jdiFrame.sourcePath, context); String methodName = formatMethodName(jdiFrame.methodName, jdiFrame.argumentTypeNames, jdiFrame.typeName, true, true); @@ -185,7 +188,27 @@ private Types.StackFrame convertDebuggerStackFrameToClient(StackFrameInfo jdiFra clientSource = null; } } - return new Types.StackFrame(frameId, methodName, clientSource, clientLineNumber, context.isClientColumnsStartAt1() ? 1 : 0, presentationHint); + + int clientColumnNumber = context.isClientColumnsStartAt1() ? 1 : 0; + // If the top-level frame is a lambda method, it might be paused on a lambda breakpoint. + // We can associate its column number with the target lambda breakpoint. + if (isTopFrame && jdiFrame.methodName.startsWith("lambda$")) { + for (IBreakpoint breakpoint : context.getBreakpointManager().getBreakpoints()) { + if (breakpoint.getColumnNumber() > 0 && breakpoint.getLineNumber() == jdiFrame.lineNumber + && Objects.equals(jdiFrame.typeName, breakpoint.className())) { + boolean match = breakpoint.requests().stream().anyMatch(request -> { + return request instanceof BreakpointRequest + && Objects.equals(((BreakpointRequest) request).location(), jdiFrame.location); + }); + if (match) { + clientColumnNumber = AdapterUtils.convertColumnNumber(breakpoint.getColumnNumber(), + context.isDebuggerColumnsStartAt1(), context.isClientColumnsStartAt1()); + } + } + } + } + + return new Types.StackFrame(frameId, methodName, clientSource, clientLineNumber, clientColumnNumber, presentationHint); } /** diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java index 9b444155c..0301bdb38 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -17,6 +17,7 @@ import com.google.gson.annotations.SerializedName; import com.microsoft.java.debug.core.protocol.Types.DataBreakpoint; +import com.microsoft.java.debug.core.protocol.Types.Source; /** * The request arguments types defined by VSCode Debug Protocol. @@ -376,6 +377,42 @@ public boolean equals(Object obj) { } } + /** + * Arguments for breakpointLocations request. + */ + public static class BreakpointLocationsArguments extends Arguments { + /** + * The source location of the breakpoints; either `source.path` or + * `source.reference` must be specified. + */ + public Source source; + + /** + * Start line of range to search possible breakpoint locations in. If only the + * line is specified, the request returns all possible locations in that line. + */ + public int line; + + /** + * Start column of range to search possible breakpoint locations in. If no + * start column is given, the first column in the start line is assumed. + */ + public int column; + + /** + * End line of range to search possible breakpoint locations in. If no end + * line is given, then the end line is assumed to be the start line. + */ + public int endLine; + + /** + * End column of range to search possible breakpoint locations in. If no end + * column is given, then it is assumed to be in the last column of the end + * line. + */ + public int endColumn; + } + public static enum Command { INITIALIZE("initialize", InitializeArguments.class), LAUNCH("launch", LaunchArguments.class), @@ -411,6 +448,7 @@ public static enum Command { INLINEVALUES("inlineValues", InlineValuesArguments.class), REFRESHVARIABLES("refreshVariables", RefreshVariablesArguments.class), PROCESSID("processId", Arguments.class), + BREAKPOINTLOCATIONS("breakpointLocations", BreakpointLocationsArguments.class), UNSUPPORTED("", Arguments.class); private String command; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java index 2210c8e93..ea660f812 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2019 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,6 +13,7 @@ import java.util.List; +import com.microsoft.java.debug.core.protocol.Types.BreakpointLocation; import com.microsoft.java.debug.core.protocol.Types.DataBreakpointAccessType; import com.microsoft.java.debug.core.protocol.Types.ExceptionBreakMode; import com.microsoft.java.debug.core.protocol.Types.ExceptionDetails; @@ -282,6 +283,21 @@ public DataBreakpointInfoResponseBody(String dataId, String description, DataBre } } + /** + * Response to breakpointLocations request. + * Contains possible locations for source breakpoints. + */ + public static class BreakpointLocationsResponseBody extends ResponseBody { + /** + * Sorted set of possible breakpoint locations. + */ + public BreakpointLocation[] breakpoints; + + public BreakpointLocationsResponseBody(BreakpointLocation[] breakpoints) { + this.breakpoints = breakpoints; + } + } + public static class ContinueResponseBody extends ResponseBody { public boolean allThreadsContinued; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java index 979c7d632..4781c7a57 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2019 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -16,7 +16,7 @@ import com.google.gson.annotations.SerializedName; /** - * The data types defined by VSCode Debug Protocol. + * The data types defined by Debug Adapter Protocol. */ public class Types { public static class Message { @@ -200,6 +200,9 @@ public Breakpoint(int id, boolean verified, int line, String message) { } } + /** + * Properties of a breakpoint or logpoint passed to the setBreakpoints request. + */ public static class SourceBreakpoint { public int line; public int column; @@ -207,7 +210,9 @@ public static class SourceBreakpoint { public String condition; public String logMessage; - public SourceBreakpoint() { + public SourceBreakpoint(int line, int column) { + this.line = line; + this.column = column; } /** @@ -300,6 +305,46 @@ public DataBreakpoint(String dataId, DataBreakpointAccessType accessType, String } } + /** + * Properties of a breakpoint location returned from the breakpointLocations request. + */ + public static class BreakpointLocation { + /** + * Start line of breakpoint location. + */ + public int line; + + /** + * The start column of breakpoint location. + */ + public int column; + + /** + * The end line of breakpoint location if the location covers a range. + */ + public int endLine; + + /** + * The end column of breakpoint location if the location covers a range. + */ + public int endColumn; + + public BreakpointLocation() { + } + + public BreakpointLocation(int line, int column) { + this.line = line; + this.column = column; + } + + public BreakpointLocation(int line, int column, int endLine, int endColumn) { + this.line = line; + this.column = column; + this.endLine = endLine; + this.endColumn = endColumn; + } + } + public static class CompletionItem { public String label; public String text; @@ -386,5 +431,7 @@ public static class Capabilities { public boolean supportsDataBreakpoints; public boolean supportsClipboardContext; public boolean supportsFunctionBreakpoints; + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_BreakpointLocations + public boolean supportsBreakpointLocationsRequest; } } diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/LambdaExpressionLocator.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/LambdaExpressionLocator.java index 351be8cb3..b37ff7b1f 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/LambdaExpressionLocator.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/LambdaExpressionLocator.java @@ -34,13 +34,9 @@ public LambdaExpressionLocator(CompilationUnit compilationUnit, int line, int co @Override public boolean visit(LambdaExpression node) { - // we only support inline breakpoints which are added before the expression part of the - // lambda. And we don't support lambda blocks since they can be debugged using line - // breakpoints. if (column > -1) { int startPosition = node.getStartPosition(); - int endPosition = node.getBody().getStartPosition(); - int offset = this.compilationUnit.getPosition(line, column); + int breakOffset = this.compilationUnit.getPosition(line, column); // lambda on same line: // list.stream().map(i -> i + 1); // @@ -48,7 +44,10 @@ public boolean visit(LambdaExpression node) { // list.stream().map(user // -> user.isSystem() ? new SystemUser(user) : new EndUser(user)); - if (offset >= startPosition && offset <= endPosition) { + // Since the debugger supports BreakpointLocations Request to hint user + // about possible inline breakpoint locations, we will only support + // inline breakpoints added where a lambda expression begins. + if (breakOffset == startPosition) { this.lambdaMethodBinding = node.resolveMethodBinding(); this.found = true; this.lambdaExpression = node; diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java index 1dc1c0e64..92ce9477a 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2021 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,12 +19,15 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; @@ -39,7 +42,9 @@ import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.LambdaExpression; import org.eclipse.jdt.internal.core.JarPackageFragmentRoot; import org.eclipse.jdt.launching.IVMInstall; import org.eclipse.jdt.launching.JavaRuntime; @@ -49,10 +54,13 @@ import com.microsoft.java.debug.LambdaExpressionLocator; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugException; +import com.microsoft.java.debug.core.JavaBreakpointLocation; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.Constants; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider; +import com.microsoft.java.debug.core.protocol.Types.BreakpointLocation; +import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint; public class JdtSourceLookUpProvider implements ISourceLookUpProvider { private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); @@ -112,12 +120,38 @@ public String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) th return new String[0]; } + SourceBreakpoint[] sourceBreakpoints = new SourceBreakpoint[lines.length]; + for (int i = 0; i < lines.length; i++) { + sourceBreakpoints[i] = new SourceBreakpoint(lines[i], columns[i]); + } + + JavaBreakpointLocation[] locations = getBreakpointLocations(uri, sourceBreakpoints); + return Stream.of(locations).map(location -> { + if (location.className() != null && location.methodName() != null) { + return location.className() + .concat("#").concat(location.methodName()) + .concat("#").concat(location.methodSignature()); + } + return location.className(); + }).toArray(String[]::new); + } + + @Override + public JavaBreakpointLocation[] getBreakpointLocations(String sourceUri, SourceBreakpoint[] sourceBreakpoints) throws DebugException { + if (sourceUri == null) { + throw new IllegalArgumentException("sourceUri is null"); + } + + if (sourceBreakpoints == null || sourceBreakpoints.length == 0) { + return new JavaBreakpointLocation[0]; + } + final ASTParser parser = ASTParser.newParser(this.latestASTLevel); parser.setResolveBindings(true); parser.setBindingsRecovery(true); parser.setStatementsRecovery(true); CompilationUnit astUnit = null; - String filePath = AdapterUtils.toPath(uri); + String filePath = AdapterUtils.toPath(sourceUri); // For file uri, read the file contents directly and pass them to the ast parser. if (filePath != null && Files.isRegularFile(Paths.get(filePath))) { String source = readFile(filePath); @@ -146,27 +180,41 @@ public String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) th } else { // For non-file uri (e.g. jdt://contents/rt.jar/java.io/PrintStream.class), // leverage jdt to load the source contents. - ITypeRoot typeRoot = resolveClassFile(uri); + ITypeRoot typeRoot = resolveClassFile(sourceUri); if (typeRoot != null) { parser.setSource(typeRoot); astUnit = (CompilationUnit) parser.createAST(null); } } - String[] fqns = new String[lines.length]; + JavaBreakpointLocation[] sourceLocations = Stream.of(sourceBreakpoints) + .map(sourceBreakpoint -> new JavaBreakpointLocation(sourceBreakpoint.line, sourceBreakpoint.column)) + .toArray(JavaBreakpointLocation[]::new); if (astUnit != null) { - for (int i = 0; i < lines.length; i++) { - if (columns[i] > -1) { + Map resolvedLocations = new HashMap<>(); + for (JavaBreakpointLocation sourceLocation : sourceLocations) { + int sourceLine = sourceLocation.lineNumber(); + int sourceColumn = sourceLocation.columnNumber(); + if (sourceColumn > -1) { // if we have a column, try to find the lambda expression at that column - LambdaExpressionLocator lambdaExpressionLocator = new LambdaExpressionLocator(astUnit, lines[i], - columns[i]); + LambdaExpressionLocator lambdaExpressionLocator = new LambdaExpressionLocator(astUnit, + sourceLine, sourceColumn); astUnit.accept(lambdaExpressionLocator); if (lambdaExpressionLocator.isFound()) { - fqns[i] = lambdaExpressionLocator.getFullyQualifiedTypeName().concat("#") - .concat(lambdaExpressionLocator.getMethodName()) - .concat("#").concat(lambdaExpressionLocator.getMethodSignature()); - continue; + sourceLocation.setClassName(lambdaExpressionLocator.getFullyQualifiedTypeName()); + sourceLocation.setMethodName(lambdaExpressionLocator.getMethodName()); + sourceLocation.setMethodSignature(lambdaExpressionLocator.getMethodSignature()); } + + if (resolvedLocations.containsKey(sourceLine)) { + sourceLocation.setAvailableBreakpointLocations(resolvedLocations.get(sourceLine)); + } else { + BreakpointLocation[] inlineLocations = getInlineBreakpointLocations(astUnit, sourceLine); + sourceLocation.setAvailableBreakpointLocations(inlineLocations); + resolvedLocations.put(sourceLine, inlineLocations); + } + + continue; } // TODO @@ -181,24 +229,56 @@ public String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) th // mark it as "unverified". // In future, we could consider supporting to update the breakpoint to a valid // location. - BreakpointLocationLocator locator = new BreakpointLocationLocator(astUnit, lines[i], true, - true); + BreakpointLocationLocator locator = new BreakpointLocationLocator(astUnit, + sourceLine, true, true); astUnit.accept(locator); // When the final valid line location is same as the original line, that // represents it's a valid breakpoint. // Add location type check to avoid breakpoint on method/field which will never // be hit in current implementation. - if (lines[i] == locator.getLineLocation() + if (sourceLine == locator.getLineLocation() && locator.getLocationType() == BreakpointLocationLocator.LOCATION_LINE) { - fqns[i] = locator.getFullyQualifiedTypeName(); + sourceLocation.setClassName(locator.getFullyQualifiedTypeName()); + if (resolvedLocations.containsKey(sourceLine)) { + sourceLocation.setAvailableBreakpointLocations(resolvedLocations.get(sourceLine)); + } else { + BreakpointLocation[] inlineLocations = getInlineBreakpointLocations(astUnit, sourceLine); + sourceLocation.setAvailableBreakpointLocations(inlineLocations); + resolvedLocations.put(sourceLine, inlineLocations); + } } else if (locator.getLocationType() == BreakpointLocationLocator.LOCATION_METHOD) { - fqns[i] = locator.getFullyQualifiedTypeName().concat("#") - .concat(locator.getMethodName()) - .concat("#").concat(locator.getMethodSignature()); + sourceLocation.setClassName(locator.getFullyQualifiedTypeName()); + sourceLocation.setMethodName(locator.getMethodName()); + sourceLocation.setMethodSignature(locator.getMethodSignature()); } } } - return fqns; + + return sourceLocations; + } + + private BreakpointLocation[] getInlineBreakpointLocations(final CompilationUnit astUnit, int sourceLine) { + List locations = new ArrayList<>(); + // The starting position of each line is the default breakpoint location for that line. + locations.add(new BreakpointLocation(sourceLine, 0)); + astUnit.accept(new ASTVisitor() { + @Override + public boolean visit(LambdaExpression node) { + int lambdaStart = node.getStartPosition(); + int startLine = astUnit.getLineNumber(lambdaStart); + if (startLine == sourceLine) { + int startColumn = astUnit.getColumnNumber(lambdaStart); + int lambdaEnd = lambdaStart + node.getLength(); + int endLine = astUnit.getLineNumber(lambdaEnd); + int endColumn = astUnit.getColumnNumber(lambdaEnd); + BreakpointLocation location = new BreakpointLocation(startLine, startColumn, endLine, endColumn); + locations.add(location); + } + return super.visit(node); + } + }); + + return locations.toArray(BreakpointLocation[]::new); } @Override