Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: debounce classpath/source change events #959

Merged
merged 1 commit into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading