diff --git a/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/actions/RestartLSPAction.java b/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/actions/RestartLSPAction.java new file mode 100644 index 0000000..bad27f2 --- /dev/null +++ b/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/actions/RestartLSPAction.java @@ -0,0 +1,60 @@ +package de.adito.aditoweb.nbm.nodejs.impl.actions; + +import de.adito.aditoweb.nbm.nodejs.impl.ls.TypeScriptLanguageServerProvider; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.editor.BaseAction; +import org.netbeans.modules.lsp.client.LSPBindings; +import org.netbeans.modules.lsp.client.spi.ServerRestarter; +import org.openide.awt.*; + +import javax.swing.*; +import javax.swing.text.JTextComponent; +import java.awt.event.ActionEvent; + +/** + * Action for restarting the LSP in javascript and typescript files. + * + * @author r.hartinger, 07.03.2023 + */ +@ActionReferences({ + @ActionReference(path = "Editors/text/typescript/Toolbars/Default", position = 20220), + @ActionReference(path = "Editors/text/javascript/Toolbars/Default", position = 20220) +}) +@ActionID(category = "adito/editor/toolbar", id = "de.adito.aditoweb.nbm.nodejs.impl.actions.RestartLSP") +@ActionRegistration(displayName = "Restart LSP", + iconBase = "de/adito/aditoweb/nbm/nodejs/impl/actions/restart.svg", lazy = false) +public class RestartLSPAction extends BaseAction +{ + + /** + * Creates a new instance. + */ + public RestartLSPAction() + { + super("Restart LSP"); + putValue(BaseAction.ICON_RESOURCE_PROPERTY, "de/adito/aditoweb/nbm/nodejs/impl/actions/restart.svg"); + putValue(Action.SHORT_DESCRIPTION, "Restart the server that provides autocomplete, go-to actions and other features for js/ts files"); + } + + @Override + public void actionPerformed(ActionEvent evt, JTextComponent target) + { + String mimeType = (String) target.getDocument().getProperty("mimeType"); + if (mimeType != null) + { + TypeScriptLanguageServerProvider typeScriptLanguageServerProvider = MimeLookup.getLookup(mimeType).lookup(TypeScriptLanguageServerProvider.class); + if (typeScriptLanguageServerProvider != null) + { + ServerRestarter serverRestarter = typeScriptLanguageServerProvider.getServerRestarter(); + if (serverRestarter != null) + { + synchronized (LSPBindings.class) + { + typeScriptLanguageServerProvider.stopServer(serverRestarter); + } + } + } + } + } + +} diff --git a/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/ls/TypeScriptLanguageServerProvider.java b/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/ls/TypeScriptLanguageServerProvider.java index 7a00ac9..7f5788b 100644 --- a/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/ls/TypeScriptLanguageServerProvider.java +++ b/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/ls/TypeScriptLanguageServerProvider.java @@ -4,13 +4,14 @@ import de.adito.aditoweb.nbm.nodejs.impl.*; import de.adito.notification.INotificationFacade; import io.reactivex.rxjava3.disposables.Disposable; +import lombok.Getter; import org.jetbrains.annotations.*; import org.netbeans.api.editor.mimelookup.*; import org.netbeans.modules.lsp.client.LanguageServerProviderAccessor; import org.netbeans.modules.lsp.client.spi.*; import org.openide.util.*; -import java.io.*; +import java.io.IOException; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.*; @@ -29,6 +30,10 @@ public class TypeScriptLanguageServerProvider implements LanguageServerProvider private final _NotificationHandler notificationHandler = new _NotificationHandler(); private Disposable currentDisposable; + @Getter + @Nullable + private ServerRestarter serverRestarter; + @Override public LanguageServerDescription startServer(@NotNull Lookup pLookup) { @@ -61,6 +66,7 @@ private Optional _startServer(@Nullable ServerRestart LOGGER.log(Level.INFO, "Starting TypeScript Language Server"); // restarts + serverRestarter = pServerRestarter; _handleRestartOnChange(pServerRestarter); // execute @@ -116,24 +122,32 @@ private void _handleRestartOnChange(@Nullable ServerRestarter pServerRestarter) if (pServerRestarter != null) currentDisposable = NodeJSInstaller.observeInstallation() .skip(1) // ignore initial value, we only want real changes - .subscribe(pTime -> { - LOGGER.info("Restarting TypeScript Language Server"); + .subscribe(pTime -> stopServer(pServerRestarter)); + } - // Stop Server - Optional currentServer = currentRef.get(); - if (currentServer != null && currentServer.isPresent()) //NOSONAR null is a valid value, even it is not recommended - _stopServer(currentServer.get()); + /** + * Stops the server via the given server restarter. + * + * @param pServerRestarter the server restarter with which the LSP should be stopped + */ + public void stopServer(@NotNull ServerRestarter pServerRestarter) + { + LOGGER.info("Restarting TypeScript Language Server"); - // Trigger Restart - try - { - pServerRestarter.restart(); - } - catch (Exception e) - { - LOGGER.log(Level.SEVERE, "", e); - } - }); + // Stop Server + Optional currentServer = currentRef.get(); + if (currentServer != null && currentServer.isPresent()) //NOSONAR null is a valid value, even it is not recommended + _stopServer(currentServer.get()); + + // Trigger Restart + try + { + pServerRestarter.restart(); + } + catch (Exception e) + { + LOGGER.log(Level.SEVERE, "", e); + } } /** diff --git a/src/main/resources/de/adito/aditoweb/nbm/nodejs/impl/actions/restart.svg b/src/main/resources/de/adito/aditoweb/nbm/nodejs/impl/actions/restart.svg new file mode 100644 index 0000000..0f800ec --- /dev/null +++ b/src/main/resources/de/adito/aditoweb/nbm/nodejs/impl/actions/restart.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/de/adito/aditoweb/nbm/nodejs/impl/actions/RestartLSPActionTest.java b/src/test/java/de/adito/aditoweb/nbm/nodejs/impl/actions/RestartLSPActionTest.java new file mode 100644 index 0000000..de71c01 --- /dev/null +++ b/src/test/java/de/adito/aditoweb/nbm/nodejs/impl/actions/RestartLSPActionTest.java @@ -0,0 +1,130 @@ +package de.adito.aditoweb.nbm.nodejs.impl.actions; + +import de.adito.aditoweb.nbm.nodejs.impl.ls.TypeScriptLanguageServerProvider; +import org.jetbrains.annotations.*; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.*; +import org.mockito.*; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.modules.lsp.client.spi.ServerRestarter; +import org.openide.util.Lookup; + +import javax.swing.text.*; +import java.awt.event.ActionEvent; +import java.util.*; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; + +/** + * Test class for {@link RestartLSPAction}. + * + * @author r.hartinger, 07.03.2023 + */ +class RestartLSPActionTest +{ + + /** + * Tests that a new {@link RestartLSPAction} is created with having the correct key - values. + */ + @Test + void testConstructor() + { + RestartLSPAction restartLSPAction = new RestartLSPAction(); + + Map expected = Map.of("Name", "Restart LSP", + "IconResource", "de/adito/aditoweb/nbm/nodejs/impl/actions/restart.svg", + "ShortDescription", "Restart the server that provides autocomplete, go-to actions and other features for js/ts files"); + + Map actual = new HashMap<>(); + // Transforming all key - values in the restartAction + Arrays.stream(restartLSPAction.getKeys()) + .filter(Objects::nonNull) + // keys should be strings, so we need to transform them + .filter(String.class::isInstance) + .map(String.class::cast) + // finding the value to each key and add it to the actual map + .forEach(pKey -> { + Object value = restartLSPAction.getValue(pKey); + actual.put(pKey, value); + }); + + assertEquals(expected, actual); + } + + /** + * Tests the method {@link RestartLSPAction#actionPerformed(ActionEvent, JTextComponent)} + */ + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class ActionPerformed + { + + /** + * @return the arguments for {@link #shouldActionPerformed(int, ServerRestarter, String)} + */ + @NotNull + private Stream shouldActionPerformed() + { + ServerRestarter serverRestarter = Mockito.spy(ServerRestarter.class); + + return Stream.of( + // no mime type -> will not call restart + Arguments.of(0, null, null), + Arguments.of(0, serverRestarter, null), + + // mime type is there, but the method will not find any lsp + Arguments.of(0, null, "text/json"), + Arguments.of(0, serverRestarter, "text/json"), + + // correct mime type, LSP can be restarted, if the server restarter is there + Arguments.of(0, null, "text/javascript"), + Arguments.of(0, null, "text/typescript"), + Arguments.of(1, serverRestarter, "text/javascript"), + Arguments.of(1, serverRestarter, "text/typescript") + ); + } + + /** + * Tests if the action is only performed, when an LSP is found, and it contains a ServerRestarter. + * In these tests, the {@link TypeScriptLanguageServerProvider} will only be found, if the mime type ends with {@code script} (e.g. typescript or javascript). + * + * @param pCallStopServer how often {@link TypeScriptLanguageServerProvider#stopServer(ServerRestarter)} will be called. + * @param pServerRestarter the {@link ServerRestarter} that should be returned by {@link TypeScriptLanguageServerProvider#getServerRestarter()} + * @param pMimeType the mime type that should be returned by the document in the {@link JTextComponent} + */ + @ParameterizedTest + @MethodSource + void shouldActionPerformed(int pCallStopServer, @Nullable ServerRestarter pServerRestarter, @Nullable String pMimeType) + { + Document document = Mockito.spy(Document.class); + Mockito.doReturn(pMimeType).when(document).getProperty("mimeType"); + + JTextComponent textComponent = Mockito.mock(JTextComponent.class); + Mockito.doReturn(document).when(textComponent).getDocument(); + + RestartLSPAction restartLSPAction = new RestartLSPAction(); + + try (MockedStatic mimeLookupMockedStatic = Mockito.mockStatic(MimeLookup.class)) + { + TypeScriptLanguageServerProvider typeScriptLanguageServerProvider = Mockito.spy(TypeScriptLanguageServerProvider.class); + Mockito.doReturn(pServerRestarter).when(typeScriptLanguageServerProvider).getServerRestarter(); + + Lookup lookup = Mockito.mock(Lookup.class); + Mockito.doReturn(typeScriptLanguageServerProvider).when(lookup).lookup(TypeScriptLanguageServerProvider.class); + + // return the correct lookup when the mime type ends with script + mimeLookupMockedStatic.when(() -> MimeLookup.getLookup(endsWith("script"))).thenReturn(lookup); + // return a lookup with nothing in it, when the mime type ends with json + mimeLookupMockedStatic.when(() -> MimeLookup.getLookup(endsWith("json"))).thenReturn(Mockito.mock(Lookup.class)); + + restartLSPAction.actionPerformed(null, textComponent); + + Mockito.verify(typeScriptLanguageServerProvider, Mockito.times(pCallStopServer)).stopServer(any()); + } + } + } + +} diff --git a/src/test/java/de/adito/aditoweb/nbm/nodejs/impl/options/downloader/NodeJSDownloaderImplTest.java b/src/test/java/de/adito/aditoweb/nbm/nodejs/impl/options/downloader/NodeJSDownloaderImplTest.java index 8e81620..3a86a7b 100644 --- a/src/test/java/de/adito/aditoweb/nbm/nodejs/impl/options/downloader/NodeJSDownloaderImplTest.java +++ b/src/test/java/de/adito/aditoweb/nbm/nodejs/impl/options/downloader/NodeJSDownloaderImplTest.java @@ -125,6 +125,6 @@ void shouldDownloadSuccessfully() throws Exception // check Assertions.assertTrue(invalidDownloads.isEmpty(), invalidDownloads::toString); - Assertions.assertTrue(validDownloads.containsAll(availableVersions), invalidDownloads::toString); + Assertions.assertTrue(validDownloads.containsAll(availableVersions), () -> validDownloads + " should contain all " + availableVersions); } }