From 6a64f58e9e74c24efbbd24b8e6d0c29738436079 Mon Sep 17 00:00:00 2001 From: azerr Date: Thu, 15 Jun 2023 10:37:07 +0200 Subject: [PATCH] fix: debounce classpath/source change events (#916) Fixes #916 Signed-off-by: azerr --- .../PsiMicroProfileProjectManager.java | 204 ++++++++++-------- .../quarkus/QuarkusPostStartupActivity.java | 52 ++--- .../quarkus/QuarkusProjectService.java | 54 ++--- .../ClasspathResourceChangeManager.java | 61 ++++++ .../ClasspathRessourceChangedListener.java | 131 +++++++++++ .../classpath/SourceFileChangeNotifier.java | 60 ++++++ .../quarkus/lsp/QuarkusLanguageClient.java | 33 +-- .../intellij/qute/lsp/QuteLanguageClient.java | 22 +- src/main/resources/META-INF/plugin.xml | 1 + 9 files changed, 443 insertions(+), 175 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/classpath/ClasspathResourceChangeManager.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/classpath/ClasspathRessourceChangedListener.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/classpath/SourceFileChangeNotifier.java diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/project/PsiMicroProfileProjectManager.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/project/PsiMicroProfileProjectManager.java index 5d6107144..21ec21284 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/project/PsiMicroProfileProjectManager.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/project/PsiMicroProfileProjectManager.java @@ -1,12 +1,12 @@ /******************************************************************************* -* Copyright (c) 2020 Red Hat Inc. and others. -* All rights reserved. This program and the accompanying materials -* which accompanies this distribution, and is available at -* https://www.eclipse.org/legal/epl-v20.html -* -* Contributors: -* Red Hat Inc. - initial API and implementation -*******************************************************************************/ + * Copyright (c) 2020 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ package com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project; import com.intellij.ProjectTopics; @@ -33,94 +33,114 @@ /** * {@link PsiMicroProfileProject} manager. - * + * * @author Angelo ZERR * @see https://github.com/redhat-developer/quarkus-ls/blob/master/microprofile.jdt/com.redhat.microprofile.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/project/JDTMicroProfileProjectManager.java - * */ @Service public final class PsiMicroProfileProjectManager { - public static PsiMicroProfileProjectManager getInstance(Project project) { - return ServiceManager.getService(project, PsiMicroProfileProjectManager.class); - } - - private Project project; - - private final Map projects; - private MicroProfileProjectListener microprofileProjectListener; - - private class MicroProfileProjectListener implements ModuleListener, BulkFileListener { - @Override - public void after(@NotNull List events) { - for(VFileEvent event : events) { - if ((event instanceof VFileDeleteEvent || event instanceof VFileContentChangeEvent || - event instanceof VFileCreateEvent) && isConfigSource(event.getFile())) { - Module javaProject = PsiUtilsLSImpl.getInstance(project).getModule(event.getFile()); - if (javaProject != null) { - PsiMicroProfileProject mpProject = getJDTMicroProfileProject(javaProject); - if (mpProject != null) { - mpProject.evictConfigSourcesCache(); - } - } - - } - } - } - - @Override - public void beforeModuleRemoved(@NotNull Project project, @NotNull Module module) { - evict(module); - } - - private void evict(Module javaProject) { - if (javaProject != null) { - // Remove the JDTMicroProfile project instance from the cache. - projects.remove(javaProject); - } - } - } - - private PsiMicroProfileProjectManager(Project project) { - this.project = project; - this.projects = new HashMap<>(); - initialize(); - } - - public PsiMicroProfileProject getJDTMicroProfileProject(Module project) { - return getJDTMicroProfileProject(project, true); - } - - private PsiMicroProfileProject getJDTMicroProfileProject(Module project, boolean create) { - Module javaProject = project; - PsiMicroProfileProject info = projects.get(javaProject); - if (info == null) { - if (!create) { - return null; - } - info = new PsiMicroProfileProject(javaProject); - projects.put(javaProject, info); - } - return info; - } - - public boolean isConfigSource(VirtualFile file) { - String fileName = file.getName(); - for (IConfigSourceProvider provider : IConfigSourceProvider.EP_NAME.getExtensions()) { - if (provider.isConfigSource(fileName)) { - return true; - } - } - return false; - } - - public void initialize() { - if (microprofileProjectListener != null) { - return; - } - microprofileProjectListener = new MicroProfileProjectListener(); - MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(project); - connection.subscribe(VirtualFileManager.VFS_CHANGES, microprofileProjectListener); - project.getMessageBus().connect(project).subscribe(ProjectTopics.MODULES, microprofileProjectListener); - } + private static final String JAVA_FILE_EXTENSION = "java"; + + public static PsiMicroProfileProjectManager getInstance(Project project) { + return ServiceManager.getService(project, PsiMicroProfileProjectManager.class); + } + + private Project project; + + private final Map projects; + private MicroProfileProjectListener microprofileProjectListener; + + private class MicroProfileProjectListener implements ModuleListener, BulkFileListener { + @Override + public void after(@NotNull List events) { + for (VFileEvent event : events) { + if ((event instanceof VFileDeleteEvent || event instanceof VFileContentChangeEvent || + event instanceof VFileCreateEvent) && isConfigSource(event.getFile())) { + Module javaProject = PsiUtilsLSImpl.getInstance(project).getModule(event.getFile()); + if (javaProject != null) { + PsiMicroProfileProject mpProject = getJDTMicroProfileProject(javaProject); + if (mpProject != null) { + mpProject.evictConfigSourcesCache(); + } + } + + } + } + } + + @Override + public void beforeModuleRemoved(@NotNull Project project, @NotNull Module module) { + evict(module); + } + + private void evict(Module javaProject) { + if (javaProject != null) { + // Remove the JDTMicroProfile project instance from the cache. + projects.remove(javaProject); + } + } + } + + private PsiMicroProfileProjectManager(Project project) { + this.project = project; + this.projects = new HashMap<>(); + initialize(); + } + + public PsiMicroProfileProject getJDTMicroProfileProject(Module project) { + return getJDTMicroProfileProject(project, true); + } + + private PsiMicroProfileProject getJDTMicroProfileProject(Module project, boolean create) { + Module javaProject = project; + PsiMicroProfileProject info = projects.get(javaProject); + if (info == null) { + if (!create) { + return null; + } + info = new PsiMicroProfileProject(javaProject); + projects.put(javaProject, info); + } + return info; + } + + /** + * Returns true if the given file is a MicroProfile config properties file (microprofile-config.properties, application.properties, application.yaml, etc) and false otherwise. + * + * @param file the file to check. + * @return true if the given file is a MicroProfile config properties file (microprofile-config.properties, application.properties, application.yaml, etc) and false otherwise. + */ + public boolean isConfigSource(VirtualFile file) { + if (file == null) { + return false; + } + String fileName = file.getName(); + for (IConfigSourceProvider provider : IConfigSourceProvider.EP_NAME.getExtensions()) { + if (provider.isConfigSource(fileName)) { + return true; + } + } + return false; + } + + /** + * Returns true if the given file is a Java file and false otherwise. + * + * @param file the file to check. + * @return true if the given file is a Java file and false otherwise. + */ + public boolean isJavaFile(VirtualFile file) { + return file != null && JAVA_FILE_EXTENSION.equals(file.getExtension()); + } + + public void initialize() { + if (microprofileProjectListener != null) { + return; + } + microprofileProjectListener = new MicroProfileProjectListener(); + MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(project); + connection.subscribe(VirtualFileManager.VFS_CHANGES, microprofileProjectListener); + project.getMessageBus().connect(project).subscribe(ProjectTopics.MODULES, microprofileProjectListener); + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusPostStartupActivity.java b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusPostStartupActivity.java index 38e3c0b82..830131964 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusPostStartupActivity.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusPostStartupActivity.java @@ -1,25 +1,27 @@ -/******************************************************************************* - * Copyright (c) 2019-2020 Red Hat, Inc. - * Distributed under license by Red Hat, Inc. All rights reserved. - * This program is made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, - * and is available at https://www.eclipse.org/legal/epl-v20.html - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - ******************************************************************************/ -package com.redhat.devtools.intellij.quarkus; - -import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleManager; -import com.intellij.openapi.project.DumbAware; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.startup.StartupActivity; -import org.jetbrains.annotations.NotNull; - -public class QuarkusPostStartupActivity implements StartupActivity, DumbAware { - @Override - public void runActivity(@NotNull Project project) { - QuarkusProjectService.getInstance(project); - } -} +/******************************************************************************* + * Copyright (c) 2019-2020 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.quarkus; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.StartupActivity; +import com.redhat.devtools.intellij.quarkus.classpath.ClasspathResourceChangeManager; +import org.jetbrains.annotations.NotNull; + +public class QuarkusPostStartupActivity implements StartupActivity, DumbAware { + @Override + public void runActivity(@NotNull Project project) { + ClasspathResourceChangeManager.getInstance(project); + QuarkusProjectService.getInstance(project); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusProjectService.java b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusProjectService.java index a32ec7cfc..10b980e05 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusProjectService.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusProjectService.java @@ -37,7 +37,6 @@ import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.ConcurrencyUtil; import com.intellij.util.messages.MessageBusConnection; -import com.intellij.util.messages.Topic; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.PropertiesManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProjectManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.core.ls.PsiUtilsLSImpl; @@ -53,17 +52,8 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; import java.util.stream.Collectors; public class QuarkusProjectService implements LibraryTable.Listener, BulkFileListener, ModuleListener, Disposable { @@ -77,23 +67,10 @@ public class QuarkusProjectService implements LibraryTable.Listener, BulkFileLis private Set modulesBeingEnsured = new HashSet<>(); - @Override - public void dispose() { - connection.disconnect(); - executor.shutdown(); - } - - public interface Listener { - void libraryUpdated(Library library); - void sourceUpdated(List> sources); - } - public static QuarkusProjectService getInstance(Project project) { return ServiceManager.getService(project, QuarkusProjectService.class); } - public static final Topic TOPIC = Topic.create(QuarkusProjectService.class.getName(), Listener.class); - private final MessageBusConnection connection; public QuarkusProjectService(Project project) { @@ -106,9 +83,9 @@ public QuarkusProjectService(Project project) { r -> new Thread(r, "Quarkus lib pool " + project.getName())); } LibraryTablesRegistrar.getInstance().getLibraryTable(project).addListener(this, project); - connection = ApplicationManager.getApplication().getMessageBus().connect(project); + connection = project.getMessageBus().connect(); connection.subscribe(VirtualFileManager.VFS_CHANGES, this); - project.getMessageBus().connect().subscribe(ProjectTopics.MODULES, this); + connection.subscribe(ProjectTopics.MODULES, this); processModules(); } @@ -153,7 +130,6 @@ private void handleLibraryUpdate(Library library) { }); } else { processModules().thenRun(() -> { - project.getMessageBus().syncPublisher(TOPIC).libraryUpdated(library); schemas.forEach((module, pair) -> { pair.setRight(Boolean.FALSE); }); @@ -182,7 +158,6 @@ public void after(@NotNull List events) { p.setRight(Boolean.FALSE); return p; })); - project.getMessageBus().syncPublisher(TOPIC).sourceUpdated(pairs); } } @@ -200,15 +175,15 @@ private Pair toPair(VFileEvent event) { if (module == null || module.isDisposed()) { return null; } - if (!isJavaFile(file) && !isConfigSource(file, project)) { + if (!isJavaFile(file, project) && !isConfigSource(file, project)) { return null; } // Here a java file or a microprofile-config.properties, application.properties has been changed return Pair.of(module, file); } - private static boolean isJavaFile(VirtualFile file) { - return "java".equalsIgnoreCase(file.getExtension()); + private static boolean isJavaFile(VirtualFile file, Project project) { + return PsiMicroProfileProjectManager.getInstance(project).isJavaFile(file); } private static boolean isConfigSource(VirtualFile file, Project project) { @@ -218,7 +193,7 @@ private static boolean isConfigSource(VirtualFile file, Project project) { public VirtualFile getSchema(Module module) { MutablePair schemaEntry = schemas.get(module); if (schemaEntry == null || !schemaEntry.getRight()) { - VirtualFile file = computeSchema(module, schemaEntry!=null?schemaEntry.getLeft():null); + VirtualFile file = computeSchema(module, schemaEntry != null ? schemaEntry.getLeft() : null); if (file != null) { if (schemaEntry != null) { schemaEntry.setRight(Boolean.TRUE); @@ -228,7 +203,7 @@ public VirtualFile getSchema(Module module) { } } } - return schemaEntry!=null?schemaEntry.getLeft():null; + return schemaEntry != null ? schemaEntry.getLeft() : null; } private static VirtualFile createJSONSchemaFile(String name) throws IOException { @@ -256,7 +231,7 @@ private VirtualFile computeSchema(Module module, VirtualFile schemaFile) { }); }); return schemaFile; - } catch (IOException| ProcessCanceledException e) { + } catch (IOException | ProcessCanceledException e) { LOGGER.warn(e.getLocalizedMessage(), e); } return null; @@ -276,4 +251,13 @@ public void moduleAdded(@NotNull Project project, @NotNull Module module) { public void moduleRemoved(@NotNull Project project, @NotNull Module module) { moduleChanged(module); } + + + @Override + public void dispose() { + LibraryTablesRegistrar.getInstance().getLibraryTable(project).removeListener(this); + connection.disconnect(); + executor.shutdown(); + } + } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/ClasspathResourceChangeManager.java b/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/ClasspathResourceChangeManager.java new file mode 100644 index 000000000..591b8eef3 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/ClasspathResourceChangeManager.java @@ -0,0 +1,61 @@ +package com.redhat.devtools.intellij.quarkus.classpath; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.psi.PsiManager; +import com.intellij.util.messages.MessageBusConnection; +import com.intellij.util.messages.Topic; +import com.intellij.workspaceModel.ide.WorkspaceModelTopics; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.List; +import java.util.Set; + +public class ClasspathResourceChangeManager implements Disposable { + + public static final Topic TOPIC = Topic.create(ClasspathResourceChangeManager.class.getName(), ClasspathResourceChangeManager.Listener.class); + + private final SourceFileChangeNotifier sourcesChanged; + private final MessageBusConnection connection; + private final ClasspathRessourceChangedListener listener; + + public static ClasspathResourceChangeManager getInstance(Project project) { + return ServiceManager.getService(project, ClasspathResourceChangeManager.class); + } + + public interface Listener { + + void librariesChanged(); + + void sourceFilesChanged(Set> sources); + } + + private final Project project; + + public ClasspathResourceChangeManager(Project project) { + this.project = project; + // Send source files changed in debounce mode + this.sourcesChanged = new SourceFileChangeNotifier(files -> + project.getMessageBus().syncPublisher(ClasspathResourceChangeManager.TOPIC).sourceFilesChanged(files) + ); + listener = new ClasspathRessourceChangedListener(project, sourcesChanged); + connection = project.getMessageBus().connect(); + // Track delete, create, update of file + connection.subscribe(VirtualFileManager.VFS_CHANGES, listener); + // Track end of Java libraries update + WorkspaceModelTopics.getInstance(project).subscribeAfterModuleLoading(connection, listener); + // Track update of update of Psi Java, properties files + PsiManager.getInstance(project).addPsiTreeChangeListener(listener, project); + } + + @Override + public void dispose() { + this.sourcesChanged.dispose(); + this.connection.disconnect(); + PsiManager.getInstance(project).removePsiTreeChangeListener(listener); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/ClasspathRessourceChangedListener.java b/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/ClasspathRessourceChangedListener.java new file mode 100644 index 000000000..129f14db5 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/ClasspathRessourceChangedListener.java @@ -0,0 +1,131 @@ +package com.redhat.devtools.intellij.quarkus.classpath; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.newvfs.BulkFileListener; +import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiTreeChangeAdapter; +import com.intellij.psi.PsiTreeChangeEvent; +import com.intellij.workspaceModel.ide.WorkspaceModelChangeListener; +import com.intellij.workspaceModel.storage.VersionedStorageChange; +import com.intellij.workspaceModel.storage.bridgeEntities.LibraryEntity; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; +import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProjectManager; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +class ClasspathRessourceChangedListener extends PsiTreeChangeAdapter implements WorkspaceModelChangeListener, BulkFileListener { + + private final Project project; + + private final SourceFileChangeNotifier sourcesChanged; + + ClasspathRessourceChangedListener(Project project, SourceFileChangeNotifier sourcesChanged) { + this.project = project; + this.sourcesChanged = sourcesChanged; + } + + @Override + public void beforeChanged(@NotNull VersionedStorageChange event) { + // Do nothing + } + + @Override + public void changed(@NotNull VersionedStorageChange event) { + var libraryChanges = event.getChanges(LibraryEntity.class); + if (libraryChanges.isEmpty()) { + return; + } + project.getMessageBus().syncPublisher(ClasspathResourceChangeManager.TOPIC).librariesChanged(); + } + + @Override + public void childAdded(@NotNull PsiTreeChangeEvent event) { + handleChangedPsiTree(event); + } + + @Override + public void childRemoved(@NotNull PsiTreeChangeEvent event) { + handleChangedPsiTree(event); + } + + @Override + public void childReplaced(@NotNull PsiTreeChangeEvent event) { + handleChangedPsiTree(event); + } + + @Override + public void childMoved(@NotNull PsiTreeChangeEvent event) { + handleChangedPsiTree(event); + } + + @Override + public void childrenChanged(@NotNull PsiTreeChangeEvent event) { + handleChangedPsiTree(event); + } + + @Override + public void propertyChanged(@NotNull PsiTreeChangeEvent event) { + handleChangedPsiTree(event); + } + + private void handleChangedPsiTree(PsiTreeChangeEvent event) { + PsiFile psiFile = event.getFile(); + if (psiFile == null) { + return; + } + tryToAddSourceFile(psiFile.getVirtualFile(), true); + } + + private void tryToAddSourceFile(VirtualFile file, boolean checkExistingFile) { + if (checkExistingFile && (file == null || !file.exists())) { + return; + } + if (!isJavaFile(file, project) && !isConfigSource(file, project)) { + return; + } + Module fileProject = LSPIJUtils.getProject(file); + if (fileProject == null) { + return; + } + sourcesChanged.addSourceFile(Pair.of(file, fileProject)); + } + + @Override + public void before(@NotNull List events) { + for (VFileEvent event: events) { + boolean expectedEvent = (event instanceof VFileDeleteEvent); + if (expectedEvent) { + tryToAddSourceFile(event.getFile(), false); + } + } + } + + @Override + public void after(@NotNull List events) { + for (VFileEvent event: events) { + boolean expectedEvent = (event instanceof VFileCreateEvent || event instanceof VFileContentChangeEvent); + if (expectedEvent) { + tryToAddSourceFile(event.getFile(), false); + } + } + } + + private static boolean isJavaFile(VirtualFile file, Project project) { + return PsiMicroProfileProjectManager.getInstance(project).isJavaFile(file); + } + + private static boolean isConfigSource(VirtualFile file, Project project) { + return PsiMicroProfileProjectManager.getInstance(project).isConfigSource(file); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/SourceFileChangeNotifier.java b/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/SourceFileChangeNotifier.java new file mode 100644 index 000000000..1ccd844d0 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/classpath/SourceFileChangeNotifier.java @@ -0,0 +1,60 @@ +package com.redhat.devtools.intellij.quarkus.classpath; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.vfs.VirtualFile; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.*; +import java.util.function.Consumer; + +public class SourceFileChangeNotifier implements Disposable { + + private static final long DEBOUNCE_DELAY = 1000; + private Timer debounceTimer; + private TimerTask debounceTask; + + private final Set> sourceFiles; + + private final Consumer>> notifier; + + public SourceFileChangeNotifier(Consumer>> notifier) { + this.notifier = notifier; + sourceFiles = new HashSet<>(); + } + public synchronized void addSourceFile(Pair pair) { + if (debounceTask != null) { + debounceTask.cancel(); + } + synchronized (sourceFiles) { + sourceFiles.add(pair); + } + + debounceTask = new TimerTask() { + @Override + public void run() { + synchronized (sourceFiles) { + notifier.accept(sourceFiles); + sourceFiles.clear(); + } + } + }; + + if (debounceTimer == null) { + debounceTimer = new Timer(); + } + + debounceTimer.schedule(debounceTask, DEBOUNCE_DELAY); + } + + @Override + public void dispose() { + if (debounceTask != null) { + debounceTask.cancel(); + } + if(debounceTimer != null) { + debounceTimer.cancel(); + } + + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java index da5bb6691..f23a38ae6 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lsp/QuarkusLanguageClient.java @@ -15,6 +15,7 @@ import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.messages.MessageBusConnection; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.ProjectLabelManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.PropertiesManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.PropertiesManagerForJava; @@ -24,6 +25,7 @@ import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; import com.redhat.devtools.intellij.quarkus.QuarkusProjectService; import com.redhat.devtools.intellij.lsp4ij.IndexAwareLanguageClient; +import com.redhat.devtools.intellij.quarkus.classpath.ClasspathResourceChangeManager; import org.apache.commons.lang3.tuple.Pair; import org.eclipse.lsp4j.*; import org.eclipse.lsp4mp.commons.*; @@ -41,17 +43,16 @@ import java.util.stream.Collectors; -public class QuarkusLanguageClient extends IndexAwareLanguageClient implements MicroProfileLanguageClientAPI, QuarkusProjectService.Listener { +public class QuarkusLanguageClient extends IndexAwareLanguageClient implements MicroProfileLanguageClientAPI, ClasspathResourceChangeManager.Listener { private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusLanguageClient.class); - private static final String JAVA_FILE_EXTENSION = "java"; private final MessageBusConnection connection; public QuarkusLanguageClient(Project project) { super(project); connection = project.getMessageBus().connect(project); - connection.subscribe(QuarkusProjectService.TOPIC, this); - QuarkusProjectService.getInstance(project); + connection.subscribe(ClasspathResourceChangeManager.TOPIC, this); + ClasspathResourceChangeManager.getInstance(project); } @Override @@ -70,15 +71,23 @@ private void sendPropertiesChangeEvent(List scope, } @Override - public void libraryUpdated(Library library) { + public void librariesChanged() { + if (isDisposed()) { + // The language client has been disposed, ignore the changed of Java source / microprofile-config.properties files + return; + } sendPropertiesChangeEvent(Collections.singletonList(MicroProfilePropertiesScope.dependencies), QuarkusModuleUtil.getModulesURIs(getProject())); } @Override - public void sourceUpdated(List> sources) { - List> info = sources.stream(). - filter(pair -> isJavaFile(pair.getRight()) || isConfigSource(pair.getRight(), pair.getLeft())). - map(pair -> Pair.of(PsiUtilsLSImpl.getProjectURI(pair.getLeft()), getScope(pair.getRight()))). + public void sourceFilesChanged(Set> sources) { + if (isDisposed()) { + // The language client has been disposed, ignore the changed of Java source / microprofile-config.properties files + return; + } + List> info = sources.stream() + .filter(pair -> isJavaFile(pair.getLeft()) || isConfigSource(pair.getLeft())) + .map(pair -> Pair.of(PsiUtilsLSImpl.getProjectURI(pair.getRight()), getScope(pair.getLeft()))). collect(Collectors.toList()); if (!info.isEmpty()) { sendPropertiesChangeEvent(info.stream().map(Pair::getRight).collect(Collectors.toList()), info.stream().map(Pair::getLeft).collect(Collectors.toSet())); @@ -90,11 +99,11 @@ private MicroProfilePropertiesScope getScope(VirtualFile file) { } private boolean isJavaFile(VirtualFile file) { - return JAVA_FILE_EXTENSION.equals(file.getExtension()); + return PsiMicroProfileProjectManager.getInstance(getProject()).isJavaFile(file); } - private boolean isConfigSource(VirtualFile file, Module project) { - return PsiMicroProfileProjectManager.getInstance(project.getProject()).isConfigSource(file); + private boolean isConfigSource(VirtualFile file) { + return PsiMicroProfileProjectManager.getInstance(getProject()).isConfigSource(file); } @Override diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java index 2c6c21452..932311c7b 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/lsp/QuteLanguageClient.java @@ -13,12 +13,13 @@ import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.messages.MessageBusConnection; +import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.core.ls.PsiUtilsLSImpl; import com.redhat.devtools.intellij.quarkus.QuarkusProjectService; import com.redhat.devtools.intellij.lsp4ij.IndexAwareLanguageClient; +import com.redhat.devtools.intellij.quarkus.classpath.ClasspathResourceChangeManager; import com.redhat.devtools.intellij.qute.psi.QuteSupportForJava; import com.redhat.devtools.intellij.qute.psi.QuteSupportForTemplate; import com.redhat.devtools.intellij.qute.psi.utils.PsiQuteProjectUtils; @@ -58,7 +59,7 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -public class QuteLanguageClient extends IndexAwareLanguageClient implements QuteLanguageClientAPI, QuarkusProjectService.Listener { +public class QuteLanguageClient extends IndexAwareLanguageClient implements QuteLanguageClientAPI, ClasspathResourceChangeManager.Listener { private static final Logger LOGGER = LoggerFactory.getLogger(QuteLanguageClient.class); private final MessageBusConnection connection; @@ -66,7 +67,7 @@ public class QuteLanguageClient extends IndexAwareLanguageClient implements Qute public QuteLanguageClient(Project project) { super(project); connection = project.getMessageBus().connect(project); - connection.subscribe(QuarkusProjectService.TOPIC, this); + connection.subscribe(ClasspathResourceChangeManager.TOPIC, this); QuarkusProjectService.getInstance(project); } @@ -82,7 +83,7 @@ public void dispose() { * * @param uris the project uris where the data model must be refreshed. */ - private void notifyDataModelChanged(Set uris) { + private void notifyQuteDataModelChanged(Set uris) { QuteLanguageServerAPI server = (QuteLanguageServerAPI) getLanguageServer(); if (server != null) { JavaDataModelChangeEvent event = new JavaDataModelChangeEvent(); @@ -92,28 +93,27 @@ private void notifyDataModelChanged(Set uris) { } @Override - public void libraryUpdated(Library library) { + public void librariesChanged() { if (isDisposed()) { // The language client has been disposed, ignore the changed of library return; } Set uris = new HashSet<>(); uris.add(PsiQuteProjectUtils.getProjectURI(getProject())); - notifyDataModelChanged(uris); + notifyQuteDataModelChanged(uris); } @Override - public void sourceUpdated(List> sources) { + public void sourceFilesChanged(Set> sources) { if (isDisposed()) { // The language client has been disposed, ignore the changed of Java source files return; } - Set uris = sources.stream() - .map(pair -> pair.getLeft()) - .map(module -> PsiQuteProjectUtils.getProjectURI(module)) + Set uris = sources.stream().map(pair -> pair.getRight()) + .map(module -> PsiUtilsLSImpl.getProjectURI(module)) .collect(Collectors.toSet()); if (!uris.isEmpty()) { - notifyDataModelChanged(uris); + notifyQuteDataModelChanged(uris); } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a01b9591a..7eb875b90 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -264,6 +264,7 @@ +