diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java index 3b7e77d07..2a9ce476a 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java @@ -42,6 +42,8 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceSymbol; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; @@ -63,7 +65,9 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableMap; public class CompositeLanguageServerComponents implements LanguageServerComponents { - + + private static final Logger log = LoggerFactory.getLogger(CompositeLanguageServerComponents.class); + public static class Builder { private Map> componentsByLanguageId = new HashMap<>(); @@ -258,10 +262,15 @@ public class CompositeLanguageServerComponents implements LanguageServerComponen private Optional findHandler(TextDocumentIdentifier docId) { TextDocument doc = server.getTextDocumentService().getLatestSnapshot(docId.getUri()); - LanguageId language = doc.getLanguageId(); - List subComponents = componentsByLanguageId.get(language); - if (subComponents != null) { - return subComponents.stream().filter(sc -> sc.getSemanticTokensHandler().isPresent()).map(sc -> sc.getSemanticTokensHandler().get()).findFirst(); + // Only opened docs ideally should get requests for semantic token to highlight + if (doc != null) { + LanguageId language = doc.getLanguageId(); + List subComponents = componentsByLanguageId.get(language); + if (subComponents != null) { + return subComponents.stream().filter(sc -> sc.getSemanticTokensHandler().isPresent()).map(sc -> sc.getSemanticTokensHandler().get()).findFirst(); + } + } else { + log.error("Received Semantic Tokens request for a non opened document: %s".formatted(docId.getUri())); } return Optional.empty(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensHandler.java index ec8ccd9c0..f4484124c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensHandler.java @@ -73,6 +73,9 @@ public class JdtSemanticTokensHandler implements SemanticTokensHandler { } private SemanticTokens computeTokens(List applicableTokenProviders, IJavaProject jp, CompilationUnit cu) { + if (cu == null) { + return new SemanticTokens(); + } List tokensData = applicableTokenProviders.stream().map(tp -> tp.computeTokens(jp, cu)).flatMap(t -> t.stream()).collect(Collectors.toList()); return new SemanticTokens(SemanticTokensUtils.mapTokensDataToLsp(tokensData, legend, offset -> cu.getLineNumber(offset) - 1, cu::getColumnNumber)); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java index 4567e22d6..7c61fe1a1 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2023 Pivotal, Inc. + * Copyright (c) 2017, 2024 Pivotal, Inc. * 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,6 +23,9 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.function.Function; import java.util.stream.Stream; @@ -74,6 +77,9 @@ public final class CompilationUnitCache implements DocumentContentProvider { private final Cache> projectToDocs; private final Cache, INameEnvironmentWithProgress>> lookupEnvCache; + private final ReentrantReadWriteLock environmentCacheLock = new ReentrantReadWriteLock(true); + private CompletableFuture debounceClassFileChanges = CompletableFuture.completedFuture(null); + private final Executor createCuExecutorThreadPool = Executors.newCachedThreadPool(); public CompilationUnitCache(JavaProjectFinder projectFinder, SimpleLanguageServer server, ProjectObserver projectObserver) { @@ -153,7 +159,12 @@ public final class CompilationUnitCache implements DocumentContentProvider { } if (server != null) { - ServerUtils.listenToClassFileChanges(server.getWorkspaceService().getFileObserver(), projectFinder, this::invalidateProject); + ServerUtils.listenToClassFileCreateAndChange(server.getWorkspaceService().getFileObserver(), projectFinder, jp -> { + if (!debounceClassFileChanges.isDone()) { + debounceClassFileChanges.cancel(false); + } + debounceClassFileChanges = CompletableFuture.runAsync(() -> invalidateProject(jp), CompletableFuture.delayedExecutor(100, TimeUnit.MILLISECONDS)); + }); } } @@ -200,45 +211,46 @@ public final class CompilationUnitCache implements DocumentContentProvider { logger.info("CU Cache: work item submitted for doc {}", uri.toASCIIString()); if (project != null) { - - CompilationUnit cu = null; - + ReadLock lock = environmentCacheLock.readLock(); + lock.lock(); try { - cu = requestCU(project, uri).get(); - } catch (UncheckedExecutionException e1) { - // ignore errors from rewrite parser. There could be many parser exceptions due to - // user incrementally typing code's text - return null; - } catch (InvalidCacheLoadException | CancellationException e) { - // ignore - } catch (Exception e) { - logger.error("", e); - return requestor.apply(null); - } - - if (cu != null) { + CompilationUnit cu = null; try { - projectToDocs.get(project.getLocationUri(), () -> new HashSet<>()).add(uri); - - logger.debug("CU Cache: start work on AST for {}", uri.toString()); - return requestor.apply(cu); - } - catch (CancellationException e) { - throw e; - } - catch (Exception e) { + cu = requestCU(project, uri).get(); + } catch (UncheckedExecutionException e1) { + // ignore errors from rewrite parser. There could be many parser exceptions due to + // user incrementally typing code's text + return null; + } catch (InvalidCacheLoadException | CancellationException e) { + // ignore + } catch (Exception e) { logger.error("", e); + return requestor.apply(null); } - finally { - logger.debug("CU Cache: end work on AST for {}", uri.toString()); + + if (cu != null) { + projectToDocs.get(project.getLocationUri(), () -> new HashSet<>()).add(uri); + + logger.debug("CU Cache: start work on AST for {}", uri.toString()); + return requestor.apply(cu); } } + catch (CancellationException e) { + throw e; + } + catch (Exception e) { + logger.error("", e); + } + finally { + logger.debug("CU Cache: end work on AST for {}", uri.toString()); + lock.unlock(); + } } return requestor.apply(null); } - private synchronized CompletableFuture requestCU(IJavaProject project, URI uri) throws ExecutionException { + private synchronized CompletableFuture requestCU(IJavaProject project, URI uri) { CompletableFuture cuFuture = uriToCu.getIfPresent(uri); if (cuFuture == null) { cuFuture = CompletableFuture.supplyAsync(() -> { @@ -352,7 +364,13 @@ public final class CompilationUnitCache implements DocumentContentProvider { uriToCu.invalidateAll(docUris); projectToDocs.invalidate(project.getLocationUri()); } - lookupEnvCache.invalidate(project.getLocationUri()); + WriteLock lock = environmentCacheLock.writeLock(); + lock.lock(); + try { + lookupEnvCache.invalidate(project.getLocationUri()); + } finally { + lock.unlock(); + } } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ServerUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ServerUtils.java index b07c3cc5d..6e64f26f7 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ServerUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ServerUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 VMware, Inc. + * Copyright (c) 2023, 2024 VMware, Inc. * 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,7 +13,9 @@ package org.springframework.ide.vscode.boot.java.utils; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -30,25 +32,33 @@ public class ServerUtils { private static final List CLASS_FILES_TO_WATCH_GLOB = List.of("**/*.class"); - public static void listenToClassFileChanges(FileObserver fileObserver, JavaProjectFinder projectFinder, Consumer callback) { + public static void listenToAnyClassFileChanges(FileObserver fileObserver, JavaProjectFinder projectFinder, Consumer callback) { fileObserver.onAnyChange(CLASS_FILES_TO_WATCH_GLOB, files -> handleFiles(projectFinder, files, callback)); } + public static void listenToClassFileCreateAndChange(FileObserver fileObserver, JavaProjectFinder projectFinder, Consumer callback) { + fileObserver.onCreatedOrChanged(CLASS_FILES_TO_WATCH_GLOB, files -> handleFiles(projectFinder, files, callback)); + } + private static void handleFiles(JavaProjectFinder projectFinder, String[] files, Consumer callback) { + Set projects = new LinkedHashSet<>(); for (String f : files) { URI uri = URI.create(f); TextDocumentIdentifier docId = new TextDocumentIdentifier(uri.toASCIIString()); projectFinder.find(docId).ifPresent(project -> { Path p = Paths.get(uri); if (IClasspathUtil.getOutputFolders(project.getClasspath()).anyMatch(folder -> p.startsWith(folder.toPath()))) { - try { - callback.accept(project); - } catch (Throwable t) { - log.error("", t); - } + projects.add(project); } }); } + for (IJavaProject project : projects) { + try { + callback.accept(project); + } catch (Throwable t) { + log.error("", t); + } + } }