Skip to content

Commit

Permalink
fix: debounce classpath/source change events (#916)
Browse files Browse the repository at this point in the history
Fixes #916

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Jun 22, 2023
1 parent b6b4b8d commit a3b4a35
Show file tree
Hide file tree
Showing 18 changed files with 803 additions and 352 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/IJ.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
IJ: [IC-2020.3.1, IC-2021.1, IC-2021.2, IC-2021.3, IC-2022.1, IC-2022.2, IC-2022.3, IC-2023.1]
IJ: [IC-2021.3, IC-2022.1, IC-2022.2, IC-2022.3, IC-2023.1]

steps:
- uses: actions/checkout@v2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,12 @@ public static Document getDocument(VirtualFile docFile) {
return FileDocumentManager.getInstance().getDocument(docFile);
}

public static @Nullable Module getProject(VirtualFile file) {
public static @Nullable Module getProject(@Nullable VirtualFile file) {
if (file == null) {
return null;
}
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
Module module = ReadAction.compute(() -> ProjectFileIndex.getInstance(project).getModuleForFile(file));
Module module = ReadAction.compute(() -> ProjectFileIndex.getInstance(project).getModuleForFile(file, false));
if (module != null) {
return module;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package com.redhat.devtools.intellij.lsp4mp4ij.classpath;

import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.ModuleListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.impl.libraries.LibraryEx;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.util.Pair;
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.redhat.devtools.intellij.lsp4ij.LSPIJUtils;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProjectManager;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

/**
* Classpath resource changed listener used to track update of:
*
* <ul>
* <li>library has changed.</li>
* <li>Java source file has changed.</li>
* <li>microprofile-config.properties file has changed.</li>
* </ul>
*/
class ClasspathResourceChangedListener extends PsiTreeChangeAdapter implements BulkFileListener, LibraryTable.Listener, ModuleListener {

private static final Logger LOGGER = LoggerFactory.getLogger(ClasspathResourceChangedListener.class);

private final ClasspathResourceChangedManager manager;

private final Set<Module> modulesBeingEnsured = new HashSet<>();

ClasspathResourceChangedListener(ClasspathResourceChangedManager manager) {
this.manager = manager;
}

// Track modules changed

public CompletableFuture<Void> processModules() {
var overriders = manager.getOverriders();
if (overriders.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
// Loop for each modules and process the load of libraries by using Classpath overrider.
return CompletableFuture.runAsync(() -> {
for (var module : ModuleManager.getInstance(manager.getProject()).getModules()) {
LOGGER.info("Calling ensure from processModules");
checkOverridedLibrary(module, true);
}
}, manager.getExecutor());
}

private CompletableFuture<Void> processModule(Module module) {
return checkOverridedLibrary(module, false);
}

private CompletableFuture<Void> checkOverridedLibrary(Module module, boolean sync) {
var overriders = manager.getOverriders();
if (modulesBeingEnsured.add(module)) {
if (sync) {
for (var overrider : overriders) {
overrider.overrideClasspath(module);
}
modulesBeingEnsured.remove(module);
return CompletableFuture.completedFuture(null);
} else {
return CompletableFuture.runAsync(() -> {
for (var overrider : overriders) {
overrider.overrideClasspath(module);
}
modulesBeingEnsured.remove(module);
}, manager.getExecutor());
}
}
return CompletableFuture.completedFuture(null);
}

@Override
public void moduleAdded(@NotNull Project project, @NotNull Module module) {
moduleChanged(module);
}

@Override
public void moduleRemoved(@NotNull Project project, @NotNull Module module) {
moduleChanged(module);
}

private void moduleChanged(Module module) {
LOGGER.info("Calling ensure from moduleChanged for module " + module.getName());
var overriders = manager.getOverriders();
if (!overriders.isEmpty()) {
// A module has changed, process the load of libraries by using Classpath overrider.
checkOverridedLibrary(module, false);
}
}

// Track library changes

@Override
public void afterLibraryAdded(@NotNull Library newLibrary) {
handleLibraryUpdate(newLibrary);
}

@Override
public void afterLibraryRemoved(@NotNull Library library) {
handleLibraryUpdate(library);
}

private void handleLibraryUpdate(Library library) {
LOGGER.info("handleLibraryUpdate called " + library.getName());
var project = manager.getProject();

// Notify that a library has changed.
final var notifier = manager.getResourceChangedNotifier();
notifier.addLibrary(library);

// Process classpath overriders.
var overriders = manager.getOverriders();
if (overriders.isEmpty()) {
if (library instanceof LibraryEx && ((LibraryEx) library).getModule() != null) {
var module = ((LibraryEx) library).getModule();
processModule(module).thenRun(() -> {
project.getMessageBus().syncPublisher(ClasspathResourceChangedManager.TOPIC).moduleUpdated(module);
});
} else {
processModules().thenRun(() -> {
notifier.addLibrary(library);
project.getMessageBus().syncPublisher(ClasspathResourceChangedManager.TOPIC).modulesUpdated();
});
}
}
}

// Track Psi file changes

@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) {
// A Psi file has been changed in the editor
PsiFile psiFile = event.getFile();
if (psiFile == null) {
return;
}
tryToAddSourceFile(psiFile.getVirtualFile(), true);
}

// Track file system changes

@Override
public void before(@NotNull List<? extends VFileEvent> events) {
for (VFileEvent event : events) {
boolean expectedEvent = (event instanceof VFileDeleteEvent);
if (expectedEvent) {
// A file has been deleted
// We need to track delete event in 'before' method because we need the project of the file (in after we loose this information).
tryToAddSourceFile(event.getFile(), false);
}
}
}

@Override
public void after(@NotNull List<? extends VFileEvent> events) {
for (VFileEvent event : events) {
boolean expectedEvent = (event instanceof VFileCreateEvent || event instanceof VFileContentChangeEvent);
if (expectedEvent) {
// A file has been created, updated
tryToAddSourceFile(event.getFile(), false);
}
}
}

private static boolean isJavaFile(VirtualFile file) {
return PsiMicroProfileProjectManager.isJavaFile(file);
}

private static boolean isConfigSource(VirtualFile file) {
return PsiMicroProfileProjectManager.isConfigSource(file);
}

private void tryToAddSourceFile(VirtualFile file, boolean checkExistingFile) {
if (checkExistingFile && (file == null || !file.exists())) {
// The file doesn't exist
return;
}
var project = manager.getProject();
if (!isJavaFile(file) && !isConfigSource(file)) {
return;
}
// The file is a Java file or microprofile-config.properties
Module module = LSPIJUtils.getProject(file);
if (module == null || module.isDisposed()) {
return;
}
// Notify that the file has changed
var notifier = manager.getResourceChangedNotifier();
notifier.addSourceFile(Pair.pair(file, module));
}

}
Loading

0 comments on commit a3b4a35

Please sign in to comment.