From fcd2f8157ad624cee6a7d57453b516c9340408c5 Mon Sep 17 00:00:00 2001 From: Achal Talati Date: Tue, 5 Dec 2023 12:05:58 +0530 Subject: [PATCH] added gototest option Signed-off-by: Achal Talati --- README.md | 1 + build.xml | 2 +- patches/6834.diff | 972 ++++++++++++++++++++++++++++++++++++++++ vscode/README.md | 1 + vscode/package.json | 19 + vscode/src/extension.ts | 42 ++ 6 files changed, 1036 insertions(+), 1 deletion(-) create mode 100644 patches/6834.diff diff --git a/README.md b/README.md index b261516..43ac4b6 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ In the VS Code command palette : * Debugger __Java+...__ - start main class or test on selected JDK. More in [Debugger section](#debugger-and-launch-configurations) * __Test Explorer__ for Java tests results visualization and execution including editor code Lenses. * Maven and Gradle support including multi-project projects, subprojects opening and Gradle priming builds. +* __Java: Go To Test/Tested Class__ - Navigates to the corresponding test or source class file ## Project Explorer Project Explorer provides an overview of logical project structure, groups sources together and greatly simplifies Java package structure exploration. Project Explorer is an addition to the classical workspace explorer. Use it to build, test, execute and operate your Maven and Gradle Java projects. diff --git a/build.xml b/build.xml index 174c2cb..6920b68 100644 --- a/build.xml +++ b/build.xml @@ -31,7 +31,7 @@ - + diff --git a/patches/6834.diff b/patches/6834.diff new file mode 100644 index 0000000..f6749d9 --- /dev/null +++ b/patches/6834.diff @@ -0,0 +1,972 @@ +diff --git a/ide/gototest/apichanges.xml b/ide/gototest/apichanges.xml +new file mode 100644 +index 000000000000..7ae8b88aba2b +--- /dev/null ++++ b/ide/gototest/apichanges.xml +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Navigate To Test APIs ++ ++ ++ ++ ++ ++ Adding TestOppositeLocator ++ ++ ++ ++ ++ ++ A TestOppsiteLocator class introduced that finds one or multiple test files for a source file, and one or multiple source files for a test file. ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Navigate To Test API changes by date ++ ++ ++ ++ ++ ++ ++

Introduction

++ ++

This document lists changes made to the Navigate To Test APIs. Please ask on the ++ dev@netbeans.apache.org ++ mailing list if you have any questions about the details of a ++ change, or are wondering how to convert existing code to be compatible. ++

++ ++
++ ++

@FOOTER@

++ ++ ++
++
+diff --git a/ide/gototest/manifest.mf b/ide/gototest/manifest.mf +index a5a1398fa5af..e7d21dbec7b5 100644 +--- a/ide/gototest/manifest.mf ++++ b/ide/gototest/manifest.mf +@@ -2,4 +2,4 @@ Manifest-Version: 1.0 + OpenIDE-Module: org.netbeans.modules.gototest/1 + OpenIDE-Module-Layer: org/netbeans/modules/gototest/resources/layer.xml + OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/gototest/resources/Bundle.properties +-OpenIDE-Module-Specification-Version: 1.55 ++OpenIDE-Module-Specification-Version: 1.56 +diff --git a/ide/gototest/nbproject/project.xml b/ide/gototest/nbproject/project.xml +index 47a0a1c6e4de..a25850f94569 100644 +--- a/ide/gototest/nbproject/project.xml ++++ b/ide/gototest/nbproject/project.xml +@@ -25,6 +25,15 @@ + + org.netbeans.modules.gototest + ++ ++ org.netbeans.api.annotations.common ++ ++ ++ ++ 1 ++ 1.50 ++ ++ + + org.netbeans.modules.gsf.testrunner + +@@ -100,7 +109,7 @@ + + + +- org.openide.util.ui ++ org.openide.util + + + +@@ -108,19 +117,19 @@ + + + +- org.openide.util ++ org.openide.util.lookup + + + +- 9.3 ++ 8.0 + + + +- org.openide.util.lookup ++ org.openide.util.ui + + + +- 8.0 ++ 9.3 + + + +@@ -135,6 +144,7 @@ + + org.netbeans.modules.jackpot30.file + org.netbeans.modules.java.hints.declarative ++ org.netbeans.modules.java.lsp.server + org.netbeans.modules.junit + org.netbeans.modules.php.project + org.netbeans.modules.python.project +@@ -142,6 +152,7 @@ + org.netbeans.modules.ruby.project + org.netbeans.modules.testng + org.netbeans.modules.web.clientproject.api ++ org.netbeans.api.gototest + org.netbeans.spi.gototest + + +diff --git a/ide/gototest/src/org/netbeans/api/gototest/TestOppositesLocator.java b/ide/gototest/src/org/netbeans/api/gototest/TestOppositesLocator.java +new file mode 100644 +index 000000000000..3c448efef055 +--- /dev/null ++++ b/ide/gototest/src/org/netbeans/api/gototest/TestOppositesLocator.java +@@ -0,0 +1,266 @@ ++/* ++ * 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.gototest; ++ ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.List; ++import java.util.concurrent.CompletableFuture; ++import java.util.stream.Collectors; ++import org.netbeans.api.annotations.common.CheckForNull; ++import org.netbeans.spi.gototest.TestLocator; ++import org.netbeans.spi.gototest.TestLocator.LocationListener; ++import org.netbeans.spi.gototest.TestLocator.LocationResult; ++import org.openide.filesystems.FileObject; ++import org.openide.util.Lookup; ++import org.openide.util.NbBundle; ++import org.openide.util.NbBundle.Messages; ++import org.openide.util.RequestProcessor; ++ ++/** ++ * Find one or multiple test files for a source file, ++ * and one or multiple source files for a test file. ++ * ++ * @since 1.56 ++ */ ++public final class TestOppositesLocator { ++ ++ private static final RequestProcessor WORKER = new RequestProcessor(TestOppositesLocator.class.getName(), 1, false, false); ++ ++ /** ++ * The default instance of TestOppositesLocator. ++ * ++ * @return the default instance of TestOppositesLocator ++ */ ++ public static TestOppositesLocator getDefault() { ++ return new TestOppositesLocator(); ++ } ++ ++ private TestOppositesLocator() {} ++ ++ /** ++ * Given the file and position in the file, if the: ++ *
    ++ *
  • given file is a source file, find corresponding test file or test files, if exist.
  • ++ *
  • given file is a test file, find corresponding source file or source files, if exist.
  • ++ *
++ * ++ * @param fo the file for which the opposites should be found ++ * @param caretOffset position in the file, or {@code -1} if unknown ++ * @return a result describing either an error, or a possibly empty list of locations found; ++ * note one of {@code errorMessage} and {@code locations} is always {@code null}, ++ * and one always non-{@code null}. ++ */ ++ @NbBundle.Messages("No_Test_Or_Tested_Class_Found=No Test or Tested class found") ++ public CompletableFuture findOpposites(FileObject fo, int caretOffset) { ++ if (!isSupportedFileType(fo)) { ++ CompletableFuture result = new CompletableFuture<>(); ++ ++ result.complete(new LocatorResult(Bundle.No_Test_Or_Tested_Class_Found(), null, null)); ++ return result; ++ } ++ else { ++ return populateLocationResults(fo, caretOffset) ++ .thenApply(locations -> new LocatorResult(null, ++ locations.stream() ++ .filter(l -> l.getErrorMessage() != null) ++ .map(l -> l.getErrorMessage()) ++ .collect(Collectors.toList()), ++ locations.stream() ++ .filter(l -> l.getFileObject()!= null) ++ .map(l -> new Location(l.getFileObject(), l.getOffset())) ++ .collect(Collectors.toList()))); ++ } ++ } ++ ++ private CompletableFuture> populateLocationResults(FileObject fo, int caretOffset) { ++ Collection locators = Lookup.getDefault() ++ .lookupAll(TestLocator.class) ++ .stream() ++ .filter(tl -> tl.appliesTo(fo)) ++ .collect(Collectors.toList()); ++ CompletableFuture> result = new CompletableFuture<>(); ++ ++ result.complete(new ArrayList<>()); ++ ++ for (TestLocator locator : locators) { ++ if (locator.appliesTo(fo)) { ++ CompletableFuture> currentFuture = new CompletableFuture<>(); ++ ++ if (locator.asynchronous()) { ++ locator.findOpposite(fo, caretOffset, new LocationListener() { ++ @Override ++ public void foundLocation(FileObject fo, LocationResult location) { ++ List resultList = ++ location != null ? Collections.singletonList(location) ++ : Collections.emptyList(); ++ ++ currentFuture.complete(resultList); ++ } ++ }); ++ } else { ++ WORKER.post(() -> { ++ try { ++ LocationResult opposite = locator.findOpposite(fo, caretOffset); ++ List resultList = ++ opposite != null ? Collections.singletonList(opposite) ++ : Collections.emptyList(); ++ ++ currentFuture.complete(resultList); ++ } catch (Throwable t) { ++ currentFuture.completeExceptionally(t); ++ } ++ }); ++ } ++ ++ result = result.thenCombine(currentFuture, (accumulator, currentList) -> { ++ accumulator.addAll(currentList); ++ return accumulator; ++ }); ++ } ++ } ++ ++ return result; ++ } ++ ++ private TestLocator getLocatorFor(FileObject fo) { ++ Collection locators = Lookup.getDefault().lookupAll(TestLocator.class); ++ for (TestLocator locator : locators) { ++ if (locator.appliesTo(fo)) { ++ return locator; ++ } ++ } ++ ++ return null; ++ } ++ ++ private boolean isSupportedFileType(FileObject fo) { ++ TestLocator locator = fo != null ? getLocatorFor(fo) : null; ++ if (locator != null) { ++ return locator.getFileType(fo) != TestLocator.FileType.NEITHER; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * A description of the found opposite files. Exactly one of {@code errorMessage} ++ * {@code locations} will be non-null; ++ */ ++ public static final class LocatorResult { ++ private final String errorMessage; ++ private final Collection providerErrors; ++ private final Collection locations; ++ ++ private LocatorResult(String errorMessage, ++ List providerErrors, ++ List locations) { ++ if (errorMessage == null && locations == null) { ++ throw new IllegalArgumentException("Both errorMessage and locations is null!"); ++ } ++ if (errorMessage != null && locations != null) { ++ throw new IllegalArgumentException("Both errorMessage and locations is non-null!"); ++ } ++ if (providerErrors == null ^ locations == null) { ++ throw new IllegalArgumentException("Both providerErrors and locations must either be null or non-null"); ++ } ++ this.errorMessage = errorMessage; ++ this.providerErrors = providerErrors != null ? Collections.unmodifiableList(providerErrors) : null; ++ this.locations = locations != null ? Collections.unmodifiableList(locations) : null; ++ } ++ ++ /** ++ * Get the error message if present. ++ * ++ * @return error message ++ */ ++ public @CheckForNull String getErrorMessage() { ++ return errorMessage; ++ } ++ ++ /** ++ * Get error messages provided by the providers. ++ * ++ * @return the errors from the providers. ++ */ ++ public Collection getProviderErrors() { ++ return providerErrors; ++ } ++ ++ /** ++ * Get the locations if present. ++ * ++ * @return the found locations. ++ */ ++ public @CheckForNull Collection getLocations() { ++ return locations; ++ } ++ ++ } ++ ++ /** ++ * A description of a target location. ++ */ ++ public static final class Location { ++ private final FileObject file; ++ private final int offset; ++ ++ /** ++ * Construct a Location from a given file and offset. ++ * @param file The FileObject of the opposite file. ++ * @param offset The offset in the file, or -1 if the offset ++ * is unknown. ++ */ ++ public Location(FileObject file, int offset) { ++ this.file = file; ++ this.offset = offset; ++ } ++ ++ /** ++ * Get the FileObject associated with this location ++ * @return The FileObject for this location, or null if ++ * this is an invalid location. In that case, consult ++ * {@link #getErrorMessage} for more information. ++ */ ++ public FileObject getFileObject() { ++ return file; ++ } ++ ++ /** ++ * Get the offset associated with this location, if any. ++ * @return The offset for this location, or -1 if the offset ++ * is not known. ++ */ ++ public int getOffset() { ++ return offset; ++ } ++ ++ /** ++ * Get the proper display name for this location. ++ * ++ * @return the display name for this location ++ */ ++ @Messages("DN_Error=Error") ++ public String getDisplayName() { ++ return file != null ? file.getName() : Bundle.DN_Error(); ++ } ++ } ++ ++} +\ No newline at end of file +diff --git a/ide/gototest/src/org/netbeans/modules/gototest/GotoOppositeAction.java b/ide/gototest/src/org/netbeans/modules/gototest/GotoOppositeAction.java +index cee8765c6698..b2f7f7aabe35 100644 +--- a/ide/gototest/src/org/netbeans/modules/gototest/GotoOppositeAction.java ++++ b/ide/gototest/src/org/netbeans/modules/gototest/GotoOppositeAction.java +@@ -23,22 +23,23 @@ + import java.awt.Point; + import java.awt.Rectangle; + import java.util.Collection; +-import java.util.HashMap; +-import java.util.concurrent.Semaphore; ++import java.util.List; ++import java.util.concurrent.ExecutionException; + import java.util.logging.Level; + import java.util.logging.Logger; ++import java.util.stream.Collectors; + import javax.swing.Action; + import javax.swing.JEditorPane; + import javax.swing.SwingUtilities; + import javax.swing.text.BadLocationException; + import javax.swing.text.Document; + import javax.swing.text.JTextComponent; ++import org.netbeans.api.gototest.TestOppositesLocator; ++import org.netbeans.api.gototest.TestOppositesLocator.Location; ++import org.netbeans.api.gototest.TestOppositesLocator.LocatorResult; + import org.netbeans.modules.gsf.testrunner.ui.api.TestCreatorPanelDisplayer; + import org.netbeans.modules.gsf.testrunner.ui.api.UICommonUtils; + import org.netbeans.modules.parsing.api.Source; +-import org.netbeans.spi.gototest.TestLocator; +-import org.netbeans.spi.gototest.TestLocator.FileType; +-import org.netbeans.spi.gototest.TestLocator.LocationListener; + import org.netbeans.spi.gototest.TestLocator.LocationResult; + import org.openide.DialogDisplayer; + import org.openide.NotifyDescriptor; +@@ -64,8 +65,6 @@ + * @author Tor Norbye + */ + public class GotoOppositeAction extends CallableSystemAction { +- private HashMap locationResults = new HashMap(); +- private Semaphore lock; + + public GotoOppositeAction() { + putValue("noIconInMenu", Boolean.TRUE); //NOI18N +@@ -113,7 +112,6 @@ protected boolean asynchronous() { + } + + @Override +- @NbBundle.Messages("No_Test_Or_Tested_Class_Found=No Test or Tested class found") + public void performAction() { + int caretOffsetHolder[] = { -1 }; + final FileObject fo = getApplicableFileObject(caretOffsetHolder); +@@ -126,26 +124,38 @@ public void performAction() { + + @Override + public void run() { +- FileType currentFileType = getCurrentFileType(); +- if(currentFileType == FileType.NEITHER) { +- StatusDisplayer.getDefault().setStatusText(Bundle.No_Test_Or_Tested_Class_Found()); ++ LocatorResult opposites; ++ ++ try { ++ opposites = TestOppositesLocator.getDefault().findOpposites(fo, caretOffset).get(); ++ } catch (InterruptedException | ExecutionException ex) { ++ Exceptions.printStackTrace(ex); ++ return ; ++ } ++ ++ if (opposites.getErrorMessage() != null) { ++ StatusDisplayer.getDefault().setStatusText(opposites.getErrorMessage()); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + TestCreatorPanelDisplayer.getDefault().displayPanel(UICommonUtils.getFileObjectsFromNodes(TopComponent.getRegistry().getActivatedNodes()), null, null); + } + }); +- } +- else { +- populateLocationResults(fo, caretOffset); ++ } else { ++ opposites.getProviderErrors() ++ .stream() ++ .forEach(msg -> { ++ DialogDisplayer.getDefault().notify( ++ new NotifyDescriptor.Message(msg, NotifyDescriptor.INFORMATION_MESSAGE)); ++ }); ++ Collection locations = opposites.getLocations(); + SwingUtilities.invokeLater(new Runnable() { +- + @Override + public void run() { +- if (locationResults.size() == 1) { +- handleResult(locationResults.keySet().iterator().next()); +- } else if (locationResults.size() > 1) { +- showPopup(fo); ++ if (locations.size() == 1) { ++ handleResult(locations.iterator().next()); ++ } else if (locations.size() > 1) { ++ showPopup(fo, locations); + } + } + }); +@@ -155,82 +165,8 @@ public void run() { + } + } + +- private void populateLocationResults(FileObject fo, int caretOffset) { +- locationResults.clear(); +- +- Collection locators = Lookup.getDefault().lookupAll(TestLocator.class); +- +- int permits = 0; +- for (TestLocator locator : locators) { +- if (locator.appliesTo(fo)) { +- permits++; +- } +- } +- +- lock = new Semaphore(permits); +- try { +- lock.acquire(permits); +- } catch (InterruptedException e) { +- } +- +- for (TestLocator locator : locators) { +- if (locator.appliesTo(fo)) { +- doPopulateLocationResults(fo, caretOffset, locator); +- } +- } +- try { +- lock.acquire(permits); +- } catch (InterruptedException ex) { +- Exceptions.printStackTrace(ex); +- } +- } +- +- private void doPopulateLocationResults(FileObject fo, int caretOffset, TestLocator locator) { +- if (locator != null) { +- if (locator.appliesTo(fo)) { +- if (locator.asynchronous()) { +- locator.findOpposite(fo, caretOffset, new LocationListener() { +- +- @Override +- public void foundLocation(FileObject fo, LocationResult location) { +- if (location != null) { +- FileObject fileObject = location.getFileObject(); +- if(fileObject == null) { +- String msg = location.getErrorMessage(); +- if (msg != null) { +- DialogDisplayer.getDefault().notify( +- new NotifyDescriptor.Message(msg, NotifyDescriptor.INFORMATION_MESSAGE)); +- } +- } else { +- locationResults.put(location, fileObject.getName()); +- } +- } +- lock.release(); +- } +- }); +- } else { +- LocationResult opposite = locator.findOpposite(fo, caretOffset); +- +- if (opposite != null) { +- FileObject fileObject = opposite.getFileObject(); +- if (fileObject == null) { +- String msg = opposite.getErrorMessage(); +- if (msg != null) { +- DialogDisplayer.getDefault().notify( +- new NotifyDescriptor.Message(msg, NotifyDescriptor.INFORMATION_MESSAGE)); +- } +- } else { +- locationResults.put(opposite, fileObject.getName()); +- } +- } +- lock.release(); +- } +- } +- } +- } +- + @NbBundle.Messages("LBL_PickExpression=Go to Test") +- private void showPopup(FileObject fo) { ++ private void showPopup(FileObject fo, Collection locations) { + JTextComponent pane; + Point l = new Point(-1, -1); + +@@ -243,51 +179,20 @@ private void showPopup(FileObject fo) { + SwingUtilities.convertPointToScreen(l, pane); + + String label = Bundle.LBL_PickExpression(); +- PopupUtil.showPopup(new OppositeCandidateChooser(this, label, locationResults), label, l.x, l.y, true, -1); ++ PopupUtil.showPopup(new OppositeCandidateChooser(this, label, locations), label, l.x, l.y, true, -1); + } + } catch (BadLocationException ex) { + Logger.getLogger(GotoOppositeAction.class.getName()).log(Level.WARNING, null, ex); + } + } + +- public void handleResult(LocationResult opposite) { ++ public void handleResult(Location opposite) { + FileObject fileObject = opposite.getFileObject(); + if (fileObject != null) { + NbDocument.openDocument(fileObject, opposite.getOffset(), Line.ShowOpenType.OPEN, Line.ShowVisibilityType.FOCUS); +- } else if (opposite.getErrorMessage() != null) { +- String msg = opposite.getErrorMessage(); +- NotifyDescriptor descr = new NotifyDescriptor.Message(msg, +- NotifyDescriptor.INFORMATION_MESSAGE); +- DialogDisplayer.getDefault().notify(descr); +- } +- } +- +- private TestLocator getLocatorFor(FileObject fo) { +- Collection locators = Lookup.getDefault().lookupAll(TestLocator.class); +- for (TestLocator locator : locators) { +- if (locator.appliesTo(fo)) { +- return locator; +- } + } +- +- return null; + } + +- private FileType getFileType(FileObject fo) { +- TestLocator locator = getLocatorFor(fo); +- if (locator != null) { +- return locator.getFileType(fo); +- } +- +- return FileType.NEITHER; +- } +- +- private FileType getCurrentFileType() { +- FileObject fo = getApplicableFileObject(new int[1]); +- +- return (fo != null) ? getFileType(fo) : FileType.NEITHER; +- } +- + private FileObject getApplicableFileObject(int[] caretPosHolder) { + if (!EventQueue.isDispatchThread()) { + // Unsafe to ask for an editor pane from a random thread. +diff --git a/ide/gototest/src/org/netbeans/modules/gototest/OppositeCandidateChooser.java b/ide/gototest/src/org/netbeans/modules/gototest/OppositeCandidateChooser.java +index 8e1ec5f413c2..d1630e55eb86 100644 +--- a/ide/gototest/src/org/netbeans/modules/gototest/OppositeCandidateChooser.java ++++ b/ide/gototest/src/org/netbeans/modules/gototest/OppositeCandidateChooser.java +@@ -24,12 +24,13 @@ + import java.awt.event.FocusListener; + import java.awt.event.KeyEvent; + import java.awt.event.MouseEvent; +-import java.util.HashMap; ++import java.util.Collection; + import javax.swing.DefaultListCellRenderer; + import javax.swing.DefaultListModel; + import javax.swing.JList; + import javax.swing.JPanel; + import javax.swing.ListModel; ++import org.netbeans.api.gototest.TestOppositesLocator.Location; + import org.netbeans.spi.gototest.TestLocator.LocationResult; + + /** +@@ -39,10 +40,10 @@ + public class OppositeCandidateChooser extends JPanel implements FocusListener { + + private final String caption; +- private static HashMap toShow; ++ private static Collection toShow; + private static GotoOppositeAction action; + +- public OppositeCandidateChooser(GotoOppositeAction action, String caption, HashMap toShow) { ++ public OppositeCandidateChooser(GotoOppositeAction action, String caption, Collection toShow) { + this.caption = caption; + OppositeCandidateChooser.action = action; + OppositeCandidateChooser.toShow = toShow; +@@ -122,14 +123,14 @@ private void jList1KeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_jL + // End of variables declaration//GEN-END:variables + + private void openSelected() { +- LocationResult locator = (LocationResult) jList1.getSelectedValue(); ++ Location locator = (Location) jList1.getSelectedValue(); + action.handleResult(locator); + PopupUtil.hidePopup(); + } + + private ListModel createListModel() { + DefaultListModel dlm = new DefaultListModel(); +- for (LocationResult cand: toShow.keySet()) { ++ for (Location cand: toShow) { + dlm.addElement(cand); + } + +@@ -146,9 +147,9 @@ public Component getListCellRendererComponent( + boolean cellHasFocus) { + Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + +- if (value instanceof LocationResult) { +- LocationResult locator = (LocationResult) value; +- setText(toShow.get(locator)); ++ if (value instanceof Location) { ++ Location location = (Location) value; ++ setText(location.getDisplayName()); + } + + return c; +diff --git a/java/java.lsp.server/nbproject/project.xml b/java/java.lsp.server/nbproject/project.xml +index 2994df98cff1..f66e913e54ab 100644 +--- a/java/java.lsp.server/nbproject/project.xml ++++ b/java/java.lsp.server/nbproject/project.xml +@@ -305,6 +305,15 @@ + 1.20 + + ++ ++ org.netbeans.modules.gototest ++ ++ ++ ++ 1 ++ 1.57 ++ ++ + + org.netbeans.modules.gsf.testrunner + +diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/commands/TestOppositesCommandProvider.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/commands/TestOppositesCommandProvider.java +new file mode 100644 +index 000000000000..ca65242b6cd6 +--- /dev/null ++++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/commands/TestOppositesCommandProvider.java +@@ -0,0 +1,85 @@ ++/* ++ * 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.commands; ++ ++import com.google.gson.JsonPrimitive; ++import java.net.MalformedURLException; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collections; ++import java.util.HashSet; ++import java.util.LinkedHashSet; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.CompletableFuture; ++import org.netbeans.api.gototest.TestOppositesLocator; ++import org.netbeans.modules.java.lsp.server.Utils; ++import org.netbeans.spi.lsp.CommandProvider; ++import org.openide.DialogDisplayer; ++import org.openide.NotifyDescriptor; ++import org.openide.filesystems.FileObject; ++import org.openide.util.lookup.ServiceProvider; ++ ++@ServiceProvider(service=CommandProvider.class) ++public class TestOppositesCommandProvider implements CommandProvider { ++ ++ private static final String NBLS_GO_TO_TEST = "nbls.go.to.test"; ++ private static final Set COMMANDS = new HashSet<>(Arrays.asList(NBLS_GO_TO_TEST)); ++ ++ @Override ++ public Set getCommands() { ++ return COMMANDS; ++ } ++ ++ @Override ++ public CompletableFuture runCommand(String command, List arguments) { ++ switch (command) { ++ case NBLS_GO_TO_TEST: { ++ try { ++ String source = ((JsonPrimitive) arguments.get(0)).getAsString(); ++ FileObject file; ++ file = Utils.fromUri(source); ++ return TestOppositesLocator.getDefault().findOpposites(file, -1).thenApply(locations -> { ++ Set result = new LinkedHashSet<>(); ++ ++ if (locations.getErrorMessage() != null) { ++ DialogDisplayer.getDefault().notify( ++ new NotifyDescriptor.Message(locations.getErrorMessage(), NotifyDescriptor.ERROR_MESSAGE)); ++ } else { ++ locations.getProviderErrors() ++ .stream() ++ .forEach(msg -> { ++ DialogDisplayer.getDefault().notify( ++ new NotifyDescriptor.Message(msg, NotifyDescriptor.INFORMATION_MESSAGE)); ++ }); ++ locations.getLocations().stream().map(l -> l.getFileObject()).forEach(result::add); ++ } ++ ++ return new ArrayList<>(result); ++ }); ++ } catch (MalformedURLException ex) { ++ return CompletableFuture.completedFuture(Collections.emptyList()); ++ } ++ } ++ default: ++ throw new UnsupportedOperationException("Command not supported: " + command); ++ } ++ } ++ ++} +diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java +index 65f6514c121b..ccbad13ac82d 100644 +--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java ++++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java +@@ -91,6 +91,8 @@ + import org.netbeans.api.debugger.ActionsManager; + import org.netbeans.api.debugger.DebuggerManager; + import org.netbeans.api.editor.mimelookup.MimeLookup; ++import org.netbeans.api.gototest.TestOppositesLocator; ++import org.netbeans.api.gototest.TestOppositesLocator.LocatorResult; + import org.netbeans.api.java.classpath.ClassPath; + import org.netbeans.api.java.project.JavaProjectConstants; + import org.netbeans.api.java.queries.SourceForBinaryQuery; +diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json +index 4ef6aa9da59a..23ca6b76f127 100644 +--- a/java/java.lsp.server/vscode/package.json ++++ b/java/java.lsp.server/vscode/package.json +@@ -620,6 +620,11 @@ + "command": "testing.runAll", + "title": "Run All Tests", + "category": "Test" ++ }, ++ { ++ "command": "nbls.goto.test", ++ "title": "Go To Test/Tested class...", ++ "category": "Java" + } + ], + "keybindings": [ +@@ -646,7 +651,12 @@ + { + "command": "nbls.java.goto.super.implementation", + "when": "nbJavaLSReady && editorLangId == java && editorTextFocus && config.netbeans.javaSupport.enabled", +- "group": "navigation@100" ++ "group": "navigation@99" ++ }, ++ { ++ "command": "nbls.goto.test", ++ "when": "nbJavaLSReady && editorLangId == java", ++ "group": "navigation@100" + } + ], + "explorer/context": [ +@@ -669,6 +679,10 @@ + "command": "nbls.workspace.compile", + "when": "nbJavaLSReady && config.netbeans.javaSupport.enabled" + }, ++ { ++ "command": "nbls.goto.test", ++ "when": "nbJavaLSReady && editorLangId == java" ++ }, + { + "command": "nbls.java.goto.super.implementation", + "when": "nbJavaLSReady && editorLangId == java && config.netbeans.javaSupport.enabled" +diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts +index 0b6fdd891eb0..d9aa0898b309 100644 +--- a/java/java.lsp.server/vscode/src/extension.ts ++++ b/java/java.lsp.server/vscode/src/extension.ts +@@ -486,6 +486,47 @@ export function activate(context: ExtensionContext): VSNetBeansAPI { + throw `Client ${c} doesn't support new project`; + } + })); ++ context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.goto.test', async (ctx) => { ++ let c: LanguageClient = await client; ++ const commands = await vscode.commands.getCommands(); ++ if (commands.includes(COMMAND_PREFIX + '.go.to.test')) { ++ try { ++ const res: Array = await vscode.commands.executeCommand(COMMAND_PREFIX + '.go.to.test', contextUri(ctx)?.toString()); ++ if (res?.length) { ++ if (res.length === 1) { ++ let file = vscode.Uri.parse(res[0]); ++ await vscode.window.showTextDocument(file, { preview: false }); ++ } else { ++ const namePathMapping: { [key: string]: string } = {} ++ res.forEach(fp => { ++ const fileName = path.basename(fp); ++ namePathMapping[fileName] = fp ++ }); ++ const selected = await window.showQuickPick(Object.keys(namePathMapping), { ++ title: 'Select files to open', ++ placeHolder: 'Test files or source files associated to each other', ++ canPickMany: true ++ }); ++ if (selected) { ++ for await (const filePath of selected) { ++ let file = vscode.Uri.parse(filePath); ++ await vscode.window.showTextDocument(file, { preview: false }); ++ } ++ } else { ++ vscode.window.showInformationMessage("No file selected"); ++ } ++ } ++ } ++ else { ++ throw new Error("No corresponding file found"); ++ } ++ } catch (err) { ++ vscode.window.showInformationMessage("Source file does not have corresponding test file or vice versa"); ++ } ++ } else { ++ throw `Client ${c} doesn't support go to test`; ++ } ++ })); + context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.workspace.compile', () => + wrapCommandWithProgress(COMMAND_PREFIX + '.build.workspace', 'Compiling workspace...', log, true) + )); diff --git a/vscode/README.md b/vscode/README.md index eab359f..5b467a9 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -45,6 +45,7 @@ In the VS Code command palette : * Debugger __Java+...__ - start main class or test on selected JDK. More in [Debugger section](#debugger-and-launch-configurations) * __Test Explorer__ for Java tests results visualization and execution including editor code Lenses. * Maven and Gradle support including multi-project projects, subprojects opening and Gradle priming builds. +* __Java: Go To Test/Tested Class__ - Navigates to the corresponding test or source class file ## Project Explorer Project Explorer provides an overview of logical project structure, groups sources together and greatly simplifies Java package structure exploration. Project Explorer is an addition to the classical workspace explorer. Use it to build, test, execute and operate your Maven and Gradle Java projects. diff --git a/vscode/package.json b/vscode/package.json index fbaf356..75d6893 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -440,6 +440,11 @@ { "command": "jdk.download.jdk", "title": "Download, install and use JDK" + }, + { + "command": "jdk.goto.test", + "title": "Go To Test/Tested class...", + "category": "Java" } ], "keybindings": [ @@ -468,6 +473,11 @@ "when": "nbJdkReady && editorLangId == java && editorTextFocus", "group": "navigation@100" }, + { + "command": "jdk.goto.test", + "when": "nbJdkReady && editorLangId == java", + "group": "navigation@101" + }, { "command": "jdk.project.run", "when": "nbJdkReady && editorLangId == java && resourceExtname == .java", @@ -485,6 +495,11 @@ "when": "nbJdkReady && explorerResourceIsFolder", "group": "navigation@3" }, + { + "command": "jdk.goto.test", + "when": "nbJdkReady && resourceExtname == .java", + "group": "goto@1" + }, { "command": "jdk.project.run", "when": "nbJdkReady && resourceExtname == .java", @@ -510,6 +525,10 @@ "command": "jdk.workspace.compile", "when": "nbJdkReady" }, + { + "command": "jdk.goto.test", + "when": "nbJdkReady && editorLangId == java" + }, { "command": "jdk.java.goto.super.implementation", "when": "nbJdkReady && editorLangId == java" diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index f0abcd6..311173b 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -444,6 +444,48 @@ export function activate(context: ExtensionContext): VSNetBeansAPI { throw `Client ${c} doesn't support new project`; } })); + + context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.goto.test', async (ctx) => { + let c: LanguageClient = await client; + const commands = await vscode.commands.getCommands(); + if (commands.includes(COMMAND_PREFIX + '.go.to.test')) { + try { + const res: Array = await vscode.commands.executeCommand(COMMAND_PREFIX + '.go.to.test', contextUri(ctx)?.toString()); + if (res?.length) { + if (res.length === 1) { + let file = vscode.Uri.parse(res[0]); + await vscode.window.showTextDocument(file, { preview: false }); + } else { + const namePathMapping: { [key: string]: string } = {} + res.forEach(fp => { + const fileName = path.basename(fp); + namePathMapping[fileName] = fp + }); + const selected = await window.showQuickPick(Object.keys(namePathMapping), { + title: 'Select files to open', + placeHolder: 'Test files or source files associated to each other', + canPickMany: true + }); + if (selected) { + for await (const filePath of selected) { + let file = vscode.Uri.parse(filePath); + await vscode.window.showTextDocument(file, { preview: false }); + } + } else { + vscode.window.showInformationMessage("No file selected"); + } + } + } + else { + throw new Error("No corresponding file found"); + } + } catch (err) { + vscode.window.showInformationMessage("Source file does not have corresponding test file or vice versa"); + } + } else { + throw `Client ${c} doesn't support go to test`; + } + })); context.subscriptions.push(vscode.commands.registerCommand(COMMAND_PREFIX + ".download.jdk", async () => { openJDKSelectionView(log); })); context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.workspace.compile', () => wrapCommandWithProgress(COMMAND_PREFIX + '.build.workspace', 'Compiling workspace...', log, true)