diff --git a/changelog/0.7.0/pr-80.v2.yml b/changelog/0.7.0/pr-80.v2.yml new file mode 100644 index 0000000..461bbd7 --- /dev/null +++ b/changelog/0.7.0/pr-80.v2.yml @@ -0,0 +1,6 @@ +type: fix +fix: + description: Manage when `Loading versions...` is displayed and alert the user to + when no versions are found + links: + - https://github.com/palantir/gradle-consistent-versions-idea-plugin/pull/80 diff --git a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/CompletionRefreshUtil.java b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/CompletionRefreshUtil.java index 53f89d1..589e7d3 100644 --- a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/CompletionRefreshUtil.java +++ b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/CompletionRefreshUtil.java @@ -16,27 +16,18 @@ package com.palantir.gradle.versions.intellij; -import com.google.common.base.Suppliers; import com.intellij.codeInsight.completion.BaseCompletionService; import com.intellij.codeInsight.completion.CompletionProcess; import com.intellij.codeInsight.completion.CompletionService; import com.intellij.openapi.application.ApplicationManager; import java.lang.reflect.InvocationTargetException; -import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class CompletionRefreshUtil { private static final Logger log = LoggerFactory.getLogger(CompletionRefreshUtil.class); - public static Supplier refreshOnceSupplier() { - return Suppliers.memoize(() -> { - triggerRefresh(); - return null; - }); - } - - private static void triggerRefresh() { + public static void scheduleRefresh() { ApplicationManager.getApplication().invokeLater(() -> { CompletionService completionService = CompletionService.getCompletionService(); if (completionService == null) { diff --git a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionCompletionContributor.java b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionCompletionContributor.java index 121b75b..7f22471 100644 --- a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionCompletionContributor.java +++ b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionCompletionContributor.java @@ -15,6 +15,7 @@ */ package com.palantir.gradle.versions.intellij; +import com.google.common.collect.EvictingQueue; import com.intellij.codeInsight.completion.CompletionContributor; import com.intellij.codeInsight.completion.CompletionParameters; import com.intellij.codeInsight.completion.CompletionProvider; @@ -37,16 +38,22 @@ import java.util.AbstractMap.SimpleEntry; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; +import java.util.Objects; +import java.util.Queue; import java.util.stream.Collectors; import one.util.streamex.StreamEx; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class VersionCompletionContributor extends CompletionContributor { + private static final Logger log = LoggerFactory.getLogger(VersionCompletionContributor.class); + private final GroupPartOrPackageNameExplorer groupPartOrPackageNameExplorer = GroupPartOrPackageNameExplorer.getInstance(); private final VersionExplorer versionExplorer = VersionExplorer.getInstance(); + private final Queue loadedDependencies = EvictingQueue.create(100); + VersionCompletionContributor() { extend( CompletionType.BASIC, @@ -55,24 +62,25 @@ public class VersionCompletionContributor extends CompletionContributor { @Override public void addCompletions( CompletionParameters parameters, ProcessingContext context, CompletionResultSet resultSet) { + DependencyInfo dependencyInfo = getDependencyInfo(parameters); - DependencyGroup group = dependencyInfo.group(); - DependencyName dependencyName = dependencyInfo.dependencyName(); Project project = parameters.getOriginalFile().getProject(); CompletionSorter sorter = CompletionSorter.emptySorter().weigh(new VersionWeigher()); CompletionResultSet sortedResultSet = resultSet.withRelevanceSorter(sorter); - addLoadingElement(sortedResultSet); + if (!loadedDependencies.contains(dependencyInfo)) { + addDisplayElement(sortedResultSet, "Loading Versions..."); + } - if (!dependencyName.name().contains("*")) { - handleDependencyWithoutWildcard(sortedResultSet, project, group, dependencyName); + if (!dependencyInfo.dependencyName().name().contains("*")) { + handleDependencyWithoutWildcard(sortedResultSet, project, dependencyInfo); return; } - List groupAndDeps = collectGroupAndDeps(project, group, dependencyName); - addToResults(sortedResultSet, groupAndDeps); + List packageInRepo = collectPackageInRepo(project, dependencyInfo); + addToResults(sortedResultSet, packageInRepo, dependencyInfo); } }); } @@ -91,9 +99,9 @@ private DependencyInfo getDependencyInfo(CompletionParameters parameters) { return new DependencyInfo(group, dependencyName); } - private void addLoadingElement(CompletionResultSet sortedResultSet) { + private void addDisplayElement(CompletionResultSet sortedResultSet, String elementText) { LookupElement loadingElement = PrioritizedLookupElement.withPriority( - LookupElementBuilder.create("Loading Versions...").withInsertHandler((elementContext, item) -> { + LookupElementBuilder.create(elementText).withInsertHandler((elementContext, item) -> { // Prevent insertion elementContext .getDocument() @@ -104,53 +112,70 @@ private void addLoadingElement(CompletionResultSet sortedResultSet) { } private void handleDependencyWithoutWildcard( - CompletionResultSet sortedResultSet, - Project project, - DependencyGroup group, - DependencyName dependencyName) { - RepositoryLoader.loadRepositories(project) - .forEach(url -> addToResults(sortedResultSet, List.of(new PackageInRepo(group, dependencyName, url)))); + CompletionResultSet sortedResultSet, Project project, DependencyInfo dependencyInfo) { + + List allPackages = RepositoryLoader.loadRepositories(project).stream() + .map(url -> new PackageInRepo(dependencyInfo.group(), dependencyInfo.dependencyName(), url)) + .collect(Collectors.toList()); + addToResults(sortedResultSet, allPackages, dependencyInfo); } - private List collectGroupAndDeps( - Project project, DependencyGroup group, DependencyName dependencyName) { + private List collectPackageInRepo(Project project, DependencyInfo dependencyInfo) { - String dependencyNamePrefix = dependencyName.name().replace("*", ""); + String dependencyNamePrefix = dependencyInfo.dependencyName().name().replace("*", ""); return StreamEx.of(RepositoryLoader.loadRepositories(project)) - .flatMap(url -> StreamEx.of( - groupPartOrPackageNameExplorer.getCancelableGroupPartOrPackageName(group, url)) + .flatMap(url -> StreamEx.of(groupPartOrPackageNameExplorer.getCancelableGroupPartOrPackageName( + dependencyInfo.group(), url)) .filter(pkgName -> pkgName.name().startsWith(dependencyNamePrefix)) .map(pkgName -> new SimpleEntry<>(url, pkgName))) .map(entry -> { RepositoryUrl url = entry.getKey(); GroupPartOrPackageName pkgName = entry.getValue(); DependencyName depName = DependencyName.of(pkgName.name()); - return new PackageInRepo(group, depName, url); + return new PackageInRepo(dependencyInfo.group(), depName, url); }) .toList(); } - private void addToResults(CompletionResultSet resultSet, List groupAndDeps) { - Map versionCounts = StreamEx.of(groupAndDeps) - .flatMap(groupAndDep -> - versionExplorer - .getVersions(groupAndDep, CompletionRefreshUtil.refreshOnceSupplier()::get) - .stream()) - .collect(Collectors.toConcurrentMap( - Function.identity(), v -> new AtomicInteger(1), (existingCount, newCount) -> { - existingCount.addAndGet(newCount.get()); - return existingCount; - })); + private void addToResults(CompletionResultSet resultSet, List packageInRepo, DependencyInfo key) { + VersionsResults results = versionExplorer.getVersions(packageInRepo); + + log.debug("{} of {} futures pending", results.pendingCount(), results.computedCount() + results.pendingCount()); + + results.scheduleRunnableOnCompletion(CompletionRefreshUtil::scheduleRefresh); + + if (results.hasNoVersions()) { + addDisplayElement(resultSet, "No versions found"); + addAndRefresh(key); + } + + if (results.hasSomeVersions()) { + addAndRefresh(key); + } + + long packageCount = packageInRepo.stream() + .map(PackageInRepo::dependencyName) + .filter(Objects::nonNull) + .distinct() + .count(); + + Map versionCounts = results.getVersionCounts(); List lookupElements = versionCounts.entrySet().stream() - .map(entry -> - createLookupElement(entry.getKey(), entry.getValue().get(), groupAndDeps.size())) + .map(entry -> createLookupElement(entry.getKey(), entry.getValue(), packageCount)) .collect(Collectors.toList()); resultSet.addAllElements(lookupElements); } - private LookupElement createLookupElement(DependencyVersion version, int count, int total) { + private void addAndRefresh(DependencyInfo key) { + if (!loadedDependencies.contains(key)) { + CompletionRefreshUtil.scheduleRefresh(); + loadedDependencies.add(key); + } + } + + private LookupElement createLookupElement(DependencyVersion version, Long count, Long total) { String typeText = ((total > 1) ? count + "/" + total + " packages" : ""); if (version.isLatest()) { typeText = ((total > 1) ? "latest for " : "latest") + typeText; diff --git a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionExplorer.java b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionExplorer.java index 9fa3181..2785dde 100644 --- a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionExplorer.java +++ b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionExplorer.java @@ -30,11 +30,15 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; +import one.util.streamex.EntryStream; +import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,23 +58,27 @@ public final class VersionExplorer { .maximumSize(10000) .buildAsync(this::fetchAndParseFromUrl); - public record PackageInRepo(DependencyGroup group, DependencyName dependencyPackage, RepositoryUrl repositoryUrl) {} + public VersionsResults getVersions(List packagesInRepo) { - public Set getVersions(PackageInRepo groupAndDep, Runnable onLoadMore) { - String urlString = urlFor(groupAndDep); + Map>> alreadyInCacheOrNot = StreamEx.of(packagesInRepo) + .toMap(packageInRepo -> { + String urlString = urlFor(packageInRepo); + return Optional.ofNullable( + shortLivedVersionCache.synchronous().getIfPresent(urlString)); + }); - Optional> cachedVersions = - Optional.ofNullable(shortLivedVersionCache.synchronous().getIfPresent(urlString)); + List>> futures = EntryStream.of(alreadyInCacheOrNot) + .filterValues(Optional::isEmpty) + .mapKeyValue((packageInRepo, emptyOpt) -> shortLivedVersionCache.get(urlFor(packageInRepo))) + .toList(); - if (cachedVersions.isPresent()) { - return cachedVersions.get(); - } - - shortLivedVersionCache.get(urlString).thenAccept(result -> { - onLoadMore.run(); - }); + List> versions = EntryStream.of(alreadyInCacheOrNot) + .values() + .filter(Optional::isPresent) + .flatMap(Optional::stream) + .toList(); - return Collections.emptySet(); + return VersionsResults.of(versions, futures); } private Set fetchAndParseFromUrl(String urlString) { @@ -91,7 +99,7 @@ private Set fetchAndParseFromUrl(String urlString) { private static @NotNull String urlFor(PackageInRepo groupAndDep) { return groupAndDep.repositoryUrl().url() + groupAndDep.group().asUrlString() - + groupAndDep.dependencyPackage().name() + "/maven-metadata.xml"; + + groupAndDep.dependencyName().name() + "/maven-metadata.xml"; } private Set parseVersionsFromContent(String content) { @@ -144,4 +152,6 @@ static VersionExplorer getInstance() { } private VersionExplorer() {} + + public record PackageInRepo(DependencyGroup group, DependencyName dependencyName, RepositoryUrl repositoryUrl) {} } diff --git a/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionsResults.java b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionsResults.java new file mode 100644 index 0000000..f5d2d5f --- /dev/null +++ b/gradle-consistent-versions-idea-plugin/src/main/java/com/palantir/gradle/versions/intellij/VersionsResults.java @@ -0,0 +1,81 @@ +/* + * (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 java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.immutables.value.Value; + +@Value.Immutable +public abstract class VersionsResults { + + public abstract List> alreadyLoadedVersions(); + + public abstract List>> stillLoadingVersions(); + + public final boolean isAllComplete() { + return getIncompleteFutures().isEmpty(); + } + + public final boolean hasSomeVersions() { + return !getVersionCounts().isEmpty(); + } + + public final boolean hasNoVersions() { + return isAllComplete() && !hasSomeVersions(); + } + + public final long computedCount() { + return alreadyLoadedVersions().size(); + } + + public final long pendingCount() { + return stillLoadingVersions().size(); + } + + public final Map getVersionCounts() { + return alreadyLoadedVersions().stream() + .collect(Collectors.flatMapping( + Set::stream, Collectors.groupingBy(Function.identity(), Collectors.counting()))); + } + + public final void scheduleRunnableOnCompletion(Runnable runnable) { + List> pendingFutures = getIncompleteFutures(); + if (!pendingFutures.isEmpty()) { + CompletableFuture.anyOf(pendingFutures.toArray(new CompletableFuture[0])) + .thenRun(runnable); + } + } + + private List> getIncompleteFutures() { + return stillLoadingVersions().stream() + .filter(future -> !future.isDone()) + .collect(Collectors.toList()); + } + + public static VersionsResults of( + List> alreadyLoaded, List>> stillLoading) { + return ImmutableVersionsResults.builder() + .alreadyLoadedVersions(alreadyLoaded) + .stillLoadingVersions(stillLoading) + .build(); + } +}