From 94dd78b2fce7def6fb5508cf4fcd4c5c9f601167 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 3 Jun 2021 11:00:27 -0400 Subject: [PATCH] Support add/remove classpath listener at runtime --- .../util/SimpleLanguageServer.java | 5 +- .../vscode/boot/jdt/ls/JdtLsProjectCache.java | 186 ++++++++++++------ .../commons-vscode/src/classpath.ts | 17 +- .../vscode-spring-boot/lib/Main.ts | 7 +- 4 files changed, 146 insertions(+), 69 deletions(-) diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java index 3c7ca13c7..341cef071 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java @@ -84,7 +84,6 @@ import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.CollectionUtil; import org.springframework.ide.vscode.commons.util.text.TextDocument; -import com.google.common.collect.ImmutableList; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -418,12 +417,12 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC hasQuickFixes() || !commands.isEmpty() )) { - ImmutableList.Builder supportedCommands = ImmutableList.builder(); + List supportedCommands = new ArrayList<>(); if (hasQuickFixes()) { supportedCommands.add(CODE_ACTION_COMMAND_ID); } supportedCommands.addAll(commands.keySet()); - c.setExecuteCommandProvider(new ExecuteCommandOptions(supportedCommands.build())); + c.setExecuteCommandProvider(new ExecuteCommandOptions(supportedCommands)); } if (hasWorkspaceSymbolHandler()) { c.setWorkspaceSymbolProvider(true); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java index 8f8a67481..a3d8be8b2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java @@ -24,7 +24,10 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,16 +38,23 @@ import org.springframework.ide.vscode.commons.java.JavaProject; import org.springframework.ide.vscode.commons.java.JdtLsJavaProject; import org.springframework.ide.vscode.commons.javadoc.JdtLsJavadocProvider; import org.springframework.ide.vscode.commons.languageserver.java.ls.ClasspathListener; +import org.springframework.ide.vscode.commons.languageserver.util.ServerCapabilityInitializer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE; import org.springframework.ide.vscode.commons.util.FileObserver; import org.springframework.ide.vscode.commons.util.UriUtil; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + import reactor.core.Disposable; +import reactor.core.Disposables; import reactor.core.publisher.Mono; -public class JdtLsProjectCache implements InitializableJavaProjectsService { +public class JdtLsProjectCache implements InitializableJavaProjectsService, ServerCapabilityInitializer { + private static final String CMD_SPRING_BOOT_ENABLE_CLASSPATH_LISTENING = "sts.vscode-spring-boot.enableClasspathListening"; + private static final Duration INITIALIZE_TIMEOUT = Duration.ofSeconds(10); private static final Object JDT_SCHEME = "jdt"; @@ -55,11 +65,20 @@ public class JdtLsProjectCache implements InitializableJavaProjectsService { private Logger log = LoggerFactory.getLogger(JdtLsProjectCache.class); private List listeners = new ArrayList<>(); - private boolean projectWatchingSupported = true; - + final private ClasspathListener CLASSPATH_LISTENER = new JstLsClasspathListener(); + + final private Disposable.Swap DISPOSABLE = Disposables.swap(); + + private Mono classpathListenerRequest; + + private boolean classpathListenerEnabled; + + private boolean initialClasspathLisetnerEnable; + public JdtLsProjectCache(SimpleLanguageServer server, boolean isJandexIndex) { this.server = server; this.IS_JANDEX_INDEX = isJandexIndex; + this.initialClasspathLisetnerEnable = true; this.server .onInitialized(initialize()) .doOnSuccess((disposable) -> { @@ -69,7 +88,7 @@ public class JdtLsProjectCache implements InitializableJavaProjectsService { }) .doOnError(error -> { log.error("JDT-based JavaProject service not available!", error); - switchOffProjectObserver(); + enableClasspathListener(false); }) .toFuture(); } @@ -133,7 +152,7 @@ public class JdtLsProjectCache implements InitializableJavaProjectsService { } private void notifyProjectObserverSupported() { - log.info("Project Observer is " + (projectWatchingSupported ? "" : "not ") + "supported"); + log.info("Project Observer is " + (classpathListenerEnabled ? "" : "not ") + "supported"); synchronized (listeners) { for (Listener listener : listeners) { listener.supported(); @@ -230,61 +249,44 @@ public class JdtLsProjectCache implements InitializableJavaProjectsService { @Override public Mono initialize() { - return Mono.defer(() -> server.addClasspathListener(new ClasspathListener() { - @Override - public void changed(Event event) { - log.debug("claspath event received {}", event); - server.doOnInitialized(() -> { - //log.info("initialized.thenRun block entered"); - try { - synchronized (table) { - String uri = UriUtil.normalize(event.projectUri); - log.debug("uri = {}", uri); - if (event.deleted) { - log.debug("event.deleted = true"); - IJavaProject deleted = table.remove(uri); - if (deleted!=null) { - log.debug("removed from table = true"); - notifyDelete(deleted); - } else { - log.warn("Deleted project not removed because uri {} not found in {}", uri, table.keySet()); - } - } else { - log.debug("deleted = false"); - URI projectUri = new URI(uri); - ClasspathData classpath = new ClasspathData(event.name, event.classpath.getEntries()); - IJavaProject newProject = IS_JANDEX_INDEX - ? new JavaProject(getFileObserver(), projectUri, classpath, - JdtLsProjectCache.this) - : new JdtLsJavaProject(server.getClient(), projectUri, classpath, JdtLsProjectCache.this); - IJavaProject oldProject = table.put(uri, newProject); - if (oldProject != null) { - notifyChanged(newProject); - } else { - notifyCreated(newProject); - } - } - } - } catch (Exception e) { - log.error("", e); - } - }); - } - }) - .timeout(INITIALIZE_TIMEOUT) - .doOnSubscribe(x -> log.info("addClasspathListener ...")) - .doOnSuccess(x -> log.info("addClasspathListener DONE")) - .doOnError(t -> { - log.error("Unexpected error registering classpath listener with JDT.", t); - switchOffProjectObserver(); - })); + return Mono.defer(() -> { + log.debug("ADD CLASSPATH LISTENER enableClasspath=" + initialClasspathLisetnerEnable); + enableClasspathListener(initialClasspathLisetnerEnable); + return Mono.just(DISPOSABLE); + }); } - private void switchOffProjectObserver() { - projectWatchingSupported = false; - notifyProjectObserverSupported(); + private synchronized void enableClasspathListener(boolean enabled) { + if (classpathListenerEnabled != enabled) { + if (enabled) { + log.debug("Adding classpath listener enabled=" + enabled); + classpathListenerEnabled = true; + notifyProjectObserverSupported(); + classpathListenerRequest = server.addClasspathListener(CLASSPATH_LISTENER).timeout(INITIALIZE_TIMEOUT) + .doOnSubscribe(x -> log.info("addClasspathListener ...")) + .doOnSuccess(x -> log.info("addClasspathListener DONE")) + .doOnError(t -> { + log.error("Unexpected error registering classpath listener with JDT.", t); + enableClasspathListener(false); + }); + final Mono oldClasspathSubscription = classpathListenerRequest; + classpathListenerRequest.subscribe(d -> { + if (oldClasspathSubscription != classpathListenerRequest) { + d.dispose(); + } else { + DISPOSABLE.update(d); + } + }); + } else { + log.debug("Removing classpath listener enabled=" + enabled); + DISPOSABLE.update(Disposables.single()); + classpathListenerRequest = null; + classpathListenerEnabled = false; + notifyProjectObserverSupported(); + } + } } - + @Override public Collection all() { return table.values(); @@ -292,7 +294,75 @@ public class JdtLsProjectCache implements InitializableJavaProjectsService { @Override public boolean isSupported() { - return projectWatchingSupported; + return classpathListenerEnabled; } + @Override + public void initialize(InitializeParams p, ServerCapabilities cap) { + server.onCommand(CMD_SPRING_BOOT_ENABLE_CLASSPATH_LISTENING, params -> { + log.debug("CLASSPATH ENABLED CMD EXEC"); + if (params.getArguments().get(0) instanceof JsonPrimitive) { + boolean classpathListeningEnabled = ((JsonPrimitive)params.getArguments().get(0)).getAsBoolean(); + log.debug("Enable classpath listening: " + classpathListeningEnabled); + enableClasspathListener(classpathListeningEnabled); + } + return CompletableFuture.completedFuture(null); + }); + + log.debug("REGISTER ENABLE CLASSPATH CMD"); + JsonObject o = (JsonObject) p.getInitializationOptions(); + if (o != null) { + JsonPrimitive enable = o.getAsJsonPrimitive("enableJdtClasspath"); + if (enable != null) { + log.debug("READING INIT VALUE for classpathEnabled=" + enable.getAsBoolean()); + initialClasspathLisetnerEnable = enable.getAsBoolean(); + } + } + log.debug("INIT VALUE for classpathEnabled=" + initialClasspathLisetnerEnable); + cap.getExecuteCommandProvider().getCommands().add(CMD_SPRING_BOOT_ENABLE_CLASSPATH_LISTENING); + } + + private class JstLsClasspathListener implements ClasspathListener { + + @Override + public void changed(Event event) { + log.debug("claspath event received {}", event); + server.doOnInitialized(() -> { + //log.info("initialized.thenRun block entered"); + try { + synchronized (table) { + String uri = UriUtil.normalize(event.projectUri); + log.debug("uri = {}", uri); + if (event.deleted) { + log.debug("event.deleted = true"); + IJavaProject deleted = table.remove(uri); + if (deleted!=null) { + log.debug("removed from table = true"); + notifyDelete(deleted); + } else { + log.warn("Deleted project not removed because uri {} not found in {}", uri, table.keySet()); + } + } else { + log.debug("deleted = false"); + URI projectUri = new URI(uri); + ClasspathData classpath = new ClasspathData(event.name, event.classpath.getEntries()); + IJavaProject newProject = IS_JANDEX_INDEX + ? new JavaProject(getFileObserver(), projectUri, classpath, + JdtLsProjectCache.this) + : new JdtLsJavaProject(server.getClient(), projectUri, classpath, JdtLsProjectCache.this); + IJavaProject oldProject = table.put(uri, newProject); + if (oldProject != null) { + notifyChanged(newProject); + } else { + notifyCreated(newProject); + } + } + } + } catch (Exception e) { + log.error("", e); + } + }); + } + } + } diff --git a/vscode-extensions/commons-vscode/src/classpath.ts b/vscode-extensions/commons-vscode/src/classpath.ts index d8a2cd895..7e62005ae 100644 --- a/vscode-extensions/commons-vscode/src/classpath.ts +++ b/vscode-extensions/commons-vscode/src/classpath.ts @@ -1,18 +1,25 @@ 'use strict'; - import * as VSCode from 'vscode'; import { RequestType } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/node'; -import { workspace } from 'vscode'; + +const JDT_SERVER_STANDARD_MODE = 'Standard' +const CMD_STS_ENABLE_CLASSPATH_LISTENER = 'sts.vscode-spring-boot.enableClasspathListening'; export function registerClasspathService(client : LanguageClient) : void { + const javaExt = VSCode.extensions.getExtension('redhat.java'); + const javaApi = javaExt?.exports; + + if (javaApi) { + VSCode.commands.executeCommand(CMD_STS_ENABLE_CLASSPATH_LISTENER, javaApi.serverMode === JDT_SERVER_STANDARD_MODE); + javaApi.onDidServerModeChange(e => VSCode.commands.executeCommand(CMD_STS_ENABLE_CLASSPATH_LISTENER, javaApi.serverMode === JDT_SERVER_STANDARD_MODE)); + } + let addRequest = new RequestType("sts/addClasspathListener"); client.onRequest(addRequest, async (params: ClasspathListenerParams) => { - const jdtConfig = workspace.getConfiguration('java') - const launchMode = jdtConfig?.get('server.launchMode'); - if (launchMode === 'LightWeight') { + if (javaApi?.serverMode === 'LightWeight') { throw new Error('Classpath listener not supported while Java Language Server is in LightWeight mode'); } return await VSCode.commands.executeCommand("java.execute.workspaceCommand", "sts.java.addClasspathListener", params.callbackCommandId); diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index fbfe434cb..ad61ca6d4 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -70,9 +70,10 @@ export function activate(context: VSCode.ExtensionContext): Thenable f.uri.toString()) : null - } + initializationOptions: () => ({ + workspaceFolders: workspace.workspaceFolders ? workspace.workspaceFolders.map(f => f.uri.toString()) : null, + enableJdtClasspath: VSCode.extensions.getExtension('redhat.java')?.exports?.serverMode === 'Standard' + }) }, highlightCodeLensSettingKey: 'boot-java.highlight-codelens.on' };