diff --git a/changelog/@unreleased/pr-67.v2.yml b/changelog/@unreleased/pr-67.v2.yml new file mode 100644 index 00000000..263ab06a --- /dev/null +++ b/changelog/@unreleased/pr-67.v2.yml @@ -0,0 +1,5 @@ +type: fix +fix: + description: Don't write locks if both the locks and props change + links: + - https://github.com/palantir/gradle-consistent-versions-idea-plugin/pull/67 diff --git a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/DebouncingAsyncFileListener.java b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/DebouncingAsyncFileListener.java new file mode 100644 index 00000000..192dcdf5 --- /dev/null +++ b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/DebouncingAsyncFileListener.java @@ -0,0 +1,67 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.gradle.versions.intellij; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.vfs.AsyncFileListener; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; +import com.intellij.util.Alarm; +import com.intellij.util.SingleAlarm; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class DebouncingAsyncFileListener implements AsyncFileListener { + private static final Logger log = LoggerFactory.getLogger(DebouncingAsyncFileListener.class); + + private final AsyncFileListener delegate; + private final SingleAlarm alarm; + private final BlockingQueue bufferedEvents = new LinkedBlockingQueue<>(); + + DebouncingAsyncFileListener(AsyncFileListener delegate, int debounceDelayMillis, Disposable parentDisposable) { + this.delegate = delegate; + this.alarm = new SingleAlarm( + this::processEvents, debounceDelayMillis, parentDisposable, Alarm.ThreadToUse.POOLED_THREAD); + } + + @Nullable + @Override + public ChangeApplier prepareChange(List events) { + log.debug("Received events: {}", events); + bufferedEvents.addAll(events); + alarm.request(); + return null; + } + + private void processEvents() { + List eventsToProcess = new ArrayList<>(); + int drained = bufferedEvents.drainTo(eventsToProcess); + if (drained == 0) { + return; + } + + log.debug("Processing debounced events: {}", eventsToProcess); + AsyncFileListener.ChangeApplier applier = delegate.prepareChange(eventsToProcess); + if (applier != null) { + applier.afterVfsChange(); + } + } +} diff --git a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/FilteringAsyncFileListener.java b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/FilteringAsyncFileListener.java new file mode 100644 index 00000000..fb7425f7 --- /dev/null +++ b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/FilteringAsyncFileListener.java @@ -0,0 +1,57 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.gradle.versions.intellij; + +import com.intellij.openapi.vfs.AsyncFileListener; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; +import java.util.List; +import java.util.function.Predicate; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FilteringAsyncFileListener implements AsyncFileListener { + private static final Logger log = LoggerFactory.getLogger(FilteringAsyncFileListener.class); + + private final AsyncFileListener delegate; + private final Predicate filter; + + FilteringAsyncFileListener(AsyncFileListener delegate, Predicate filter) { + this.delegate = delegate; + this.filter = filter; + } + + @Nullable + @Override + public final ChangeApplier prepareChange(List events) { + List filteredEvents = events.stream() + .filter(event -> { + VirtualFile file = event.getFile(); + return file != null && filter.test(file); + }) + .toList(); + + log.debug("Events after filtering {}", filteredEvents); + + if (filteredEvents.isEmpty()) { + return null; + } + + return delegate.prepareChange(filteredEvents); + } +} diff --git a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionPropsFileListener.java b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionPropsFileListener.java index c8bdc13d..60072bc6 100644 --- a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionPropsFileListener.java +++ b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionPropsFileListener.java @@ -17,6 +17,7 @@ package com.palantir.gradle.versions.intellij; import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ComponentManager; import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder; import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings; @@ -25,6 +26,7 @@ import com.intellij.openapi.externalSystem.util.ExternalSystemUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.util.Computable; import com.intellij.openapi.vfs.AsyncFileListener; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent; @@ -56,6 +58,12 @@ public ChangeApplier prepareChange(List events) { .filter(event -> "versions.props".equals(event.getFile().getName())) .toList(); + List versionLockEvents = events.stream() + .filter(event -> event instanceof VFileContentChangeEvent) + .map(event -> (VFileContentChangeEvent) event) + .filter(event -> "versions.lock".equals(event.getFile().getName())) + .toList(); + if (versionPropsEvents.isEmpty()) { return null; } @@ -64,9 +72,12 @@ public ChangeApplier prepareChange(List events) { ProjectManager.getInstance().getOpenProjects()) .filter(Project::isInitialized) .filter(Predicate.not(ComponentManager::isDisposed)) + .filter(project -> project.getBasePath() != null) .filter(project -> versionPropsEvents.stream() .anyMatch(event -> event.getPath().startsWith(project.getBasePath()) && !isFileMalformed(project, event.getFile()))) + .filter(project -> versionLockEvents.stream() + .noneMatch(event -> event.getPath().startsWith(project.getBasePath()))) .toList(); return new ChangeApplier() { @@ -138,12 +149,14 @@ private void refreshProject(Project project, ImportSpecBuilder importSpec) { } private static boolean isFileMalformed(Project project, VirtualFile file) { - PsiFile psiFile = PsiManager.getInstance(project).findFile(file); + return ApplicationManager.getApplication().runReadAction((Computable) () -> { + PsiFile psiFile = PsiManager.getInstance(project).findFile(file); - if (psiFile == null || !(psiFile.getFileType() instanceof VersionPropsFileType)) { - return true; - } + if (psiFile == null || !(psiFile.getFileType() instanceof VersionPropsFileType)) { + return true; + } - return PsiTreeUtil.hasErrorElements(psiFile); + return PsiTreeUtil.hasErrorElements(psiFile); + }); } } diff --git a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionPropsListenerRegistrar.java b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionPropsListenerRegistrar.java new file mode 100644 index 00000000..d94f0b34 --- /dev/null +++ b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionPropsListenerRegistrar.java @@ -0,0 +1,48 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.gradle.versions.intellij; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.vfs.AsyncFileListener; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; +import java.util.List; +import org.jetbrains.annotations.Nullable; + +public final class VersionPropsListenerRegistrar implements AsyncFileListener, Disposable { + + private final FilteringAsyncFileListener changeListener; + + VersionPropsListenerRegistrar() { + this.changeListener = new FilteringAsyncFileListener( + new DebouncingAsyncFileListener(new VersionPropsFileListener(), 250, this), this::isRelevantFile); + } + + private boolean isRelevantFile(VirtualFile virtualFile) { + String fileName = virtualFile.getName(); + return "versions.props".equals(fileName) || "versions.lock".equals(fileName); + } + + @Nullable + @Override + public ChangeApplier prepareChange(List events) { + return changeListener.prepareChange(events); + } + + @Override + public void dispose() {} +} diff --git a/gradle-consistent-versions-idea-plugin/src/main/resources/META-INF/plugin.xml b/gradle-consistent-versions-idea-plugin/src/main/resources/META-INF/plugin.xml index 2e0331fe..96cf6628 100644 --- a/gradle-consistent-versions-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/gradle-consistent-versions-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -50,7 +50,7 @@ - +