Merge pull request #5536 from jlahoda/run-ask-main

Let the user choose main class, when none is open
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java
index 5ec31be..b79320a 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java
@@ -23,13 +23,19 @@
 import java.net.URISyntaxException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.lang.model.element.TypeElement;
 
 import org.apache.commons.lang3.StringUtils;
 import org.eclipse.lsp4j.debug.OutputEventArguments;
@@ -37,12 +43,25 @@
 import org.eclipse.lsp4j.debug.TerminatedEventArguments;
 import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
 import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.project.JavaProjectConstants;
+import org.netbeans.api.java.source.ClasspathInfo;
+import org.netbeans.api.java.source.ElementHandle;
+import org.netbeans.api.java.source.SourceUtils;
+import org.netbeans.api.project.ProjectUtils;
+import org.netbeans.modules.java.lsp.server.LspServerState;
 import org.netbeans.modules.java.lsp.server.debugging.DebugAdapterContext;
 import org.netbeans.modules.java.lsp.server.debugging.NbSourceProvider;
 import org.netbeans.modules.java.lsp.server.debugging.utils.ErrorUtilities;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.openide.DialogDescriptor;
+import org.openide.DialogDisplayer;
+import org.openide.NotifyDescriptor;
+import org.openide.NotifyDescriptor.QuickPick.Item;
 import org.openide.filesystems.FileObject;
 import org.openide.filesystems.FileUtil;
+import org.openide.util.Lookup;
 import org.openide.util.Utilities;
+import org.openide.util.lookup.Lookups;
 
 /**
  *
@@ -71,10 +90,89 @@
 
         if (!isNative && (StringUtils.isBlank(mainFilePath) && StringUtils.isBlank(filePath) && StringUtils.isBlank(projectFilePath)
                           || modulePaths.isEmpty() && classPaths.isEmpty())) {
-            ErrorUtilities.completeExceptionally(resultFuture,
-                "Failed to launch debuggee VM. Missing mainClass or modulePaths/classPaths options in launch configuration.",
-                ResponseErrorCode.ServerNotInitialized);
-            return resultFuture;
+            if (modulePaths.isEmpty() && classPaths.isEmpty()) {
+                ErrorUtilities.completeExceptionally(resultFuture,
+                    "Failed to launch debuggee VM. Missing modulePaths/classPaths options in launch configuration.",
+                    ResponseErrorCode.ServerNotInitialized);
+                return resultFuture;
+            }
+            if (StringUtils.isBlank(mainFilePath) && StringUtils.isBlank(filePath) && StringUtils.isBlank(projectFilePath)) {
+                LspServerState state = Lookup.getDefault().lookup(LspServerState.class);
+                if (state == null) {
+                    ErrorUtilities.completeExceptionally(resultFuture,
+                        "Failed to launch debuggee VM. Missing mainClass or modulePaths/classPaths options in launch configuration.",
+                        ResponseErrorCode.ServerNotInitialized);
+                    return resultFuture;
+                }
+                return state.openedProjects().thenCompose(prjs -> {
+                    FileObject[] sourceRoots =
+                        Arrays.stream(prjs)
+                              .flatMap(p -> Arrays.stream(ProjectUtils.getSources(p).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)))
+                              .map(sg -> sg.getRootFolder())
+                              .toArray(s -> new FileObject[s]);
+
+                    List<ElementHandle<TypeElement>> mainClasses =
+                            new ArrayList<>(SourceUtils.getMainClasses(sourceRoots));
+                    CompletableFuture<Void> currentResult = new CompletableFuture<>();
+                    Consumer<ElementHandle<TypeElement>> handleSelectedMainClass = mainClass -> {
+                        Map<String, Object> newLaunchArguments = new HashMap<>(launchArguments);
+                        ClasspathInfo cpInfo = ClasspathInfo.create(ClassPath.EMPTY, ClassPath.EMPTY, ClassPathSupport.createClassPath(sourceRoots));
+                        FileObject mainClassFile = SourceUtils.getFile(mainClass, cpInfo);
+                        if (mainClassFile == null) {
+                            ErrorUtilities.completeExceptionally(currentResult,
+                                "Cannot find source file for the selected main class.",
+                                ResponseErrorCode.ServerNotInitialized);
+                        } else {
+                            newLaunchArguments.put("mainClass", mainClassFile.toURI().toString());
+                            launch(newLaunchArguments, context).thenAccept(res -> currentResult.complete(res))
+                                                               .exceptionally(t -> {
+                                                                   currentResult.completeExceptionally(t);
+                                                                   return null;
+                                                               });
+                        }
+                    };
+
+                    switch (mainClasses.size()) {
+                        case 0:
+                            String message = "Failed to launch debuggee VM. No mainClass provided.";
+                            if (SourceUtils.isScanInProgress()) {
+                                message += " Indexing is in progress, please try again when the indexing is completed.";
+                            }
+                            ErrorUtilities.completeExceptionally(currentResult,
+                                message,
+                                ResponseErrorCode.ServerNotInitialized);
+                            break;
+                        case 1:
+                            handleSelectedMainClass.accept(mainClasses.get(0));
+                            break;
+                        case 2:
+                            List<NotifyDescriptor.QuickPick.Item> mainClassItems =
+                                    mainClasses.stream()
+                                               .map(eh -> new Item(eh.getQualifiedName(), eh.getQualifiedName()))
+                                               .collect(Collectors.toList());
+
+                            NotifyDescriptor.QuickPick pick = new DialogDescriptor.QuickPick("Please choose main class",
+                                                                                             "Choose main class", mainClassItems, false);
+                            DialogDisplayer.getDefault().notifyFuture(pick).thenAccept(acceptedPick -> {
+                                for (int i = 0; i < acceptedPick.getItems().size(); i++) {
+                                    NotifyDescriptor.QuickPick.Item item = acceptedPick.getItems().get(i);
+                                    if (item.isSelected()) {
+                                        ElementHandle<TypeElement> mainClass = mainClasses.get(i);
+                                        handleSelectedMainClass.accept(mainClass);
+                                        break;
+                                    }
+                                }
+                            }).exceptionally(t -> {
+                                ErrorUtilities.completeExceptionally(currentResult,
+                                    "Failed to launch debuggee VM. No mainClass provided.",
+                                    ResponseErrorCode.ServerNotInitialized);
+                                return null;
+                            });
+                            break;
+                    }
+                    return currentResult;
+                });
+            }
         }
         if (StringUtils.isBlank((String)launchArguments.get("encoding"))) {
             context.setDebuggeeEncoding(StandardCharsets.UTF_8);