diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/LoadedMavenProject.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/LoadedMavenProject.java index 7d752c1c..64bfb47d 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/LoadedMavenProject.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/LoadedMavenProject.java @@ -33,10 +33,9 @@ public class LoadedMavenProject { private final Collection problems; private final DependencyResolutionResult dependencyResolutionResult; - public LoadedMavenProject(MavenProject mavenProject, int version, Collection problems, + public LoadedMavenProject(MavenProject mavenProject, Collection problems, DependencyResolutionResult dependencyResolutionResult) { this.mavenProject = mavenProject; - this.lastCheckedVersion = version; this.problems = problems; this.dependencyResolutionResult = dependencyResolutionResult; } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/LoadedMavenProjectProvider.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/LoadedMavenProjectProvider.java new file mode 100644 index 00000000..b7b63a0c --- /dev/null +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/LoadedMavenProjectProvider.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * 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 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lemminx.extensions.maven; + +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.maven.model.building.FileModelSource; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.extensions.maven.utils.DOMModelSource; +import org.eclipse.lemminx.extensions.maven.MavenProjectCache.ProjectBuildManager; + +import org.eclipse.lemminx.services.IXMLDocumentProvider; +import org.eclipse.lemminx.utils.FilesUtils; + +/** + * An object aggregating a build results of a Maven Document, controlling the + * asynchronous access to the Maven Project built from the latest version of + * the provided document. + */ +public class LoadedMavenProjectProvider { + private static final Logger LOGGER = Logger.getLogger(LoadedMavenProjectProvider.class.getName()); + + private final String uri; + private final IXMLDocumentProvider documentProvider; + private final ProjectBuildManager buildManager; + + private int lastCheckedVersion; + private CompletableFuture future; + + /** + * Creates a LoadedMavenProjectProvider using provided URI String identifying the + * document to be built into a Maven Project. A document found by using Document + * Provider is the latest version of the document being currently edited or a document + * read from a file specified by document URI String. + * + * @param uri A URI String identifying the document + * @param documentProvider An IXMLDocumentProvider instance used to find the latest + * version of the document + * @param buildManager A MavenProject builder + */ + public LoadedMavenProjectProvider(String uri, IXMLDocumentProvider documentProvider, ProjectBuildManager buildManager) { + this.uri = uri; + this.documentProvider = documentProvider; + this.buildManager = buildManager; + this.lastCheckedVersion = -1; + } + + /** + * Returns a `CompletableFuture` for asynchronous access + * to the Maven Project built from the latest version of the document. + * + * @return CompletableFuture of LoadedMavenProject object + */ + public CompletableFuture getLoadedMavenProject() { + DOMDocument document = documentProvider.getDocument(uri); + // Check if future must be created + // 1. is the future exist? + boolean shouldLoad = future == null || future.isCompletedExceptionally(); + if (!shouldLoad) { + // 2. is the current future is not out of dated? + if (document != null) { + if (lastCheckedVersion != document.getTextDocument().getVersion()) { + shouldLoad = true; + } + } + } + + if (shouldLoad) { + if (future != null) { + future.cancel(true); + } + if (document != null) { + lastCheckedVersion = document.getTextDocument().getVersion(); + } + future = load(uri, document); + } + return future; + } + + private CompletableFuture load(String uri, DOMDocument document) { + try { + FileModelSource source = null; + if (document != null) { + source = new DOMModelSource(document); + } else { + source = new FileModelSource(FilesUtils.toFile(uri)); + } + return buildManager.build(uri, source); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage() + ": " + uri, e); + throw e; + } + } + + /** + * Returns URI String identifying a Maven Project document or file + * + * @return + */ + public String getUri() { + return uri; + } + + /** + * Returns the last checked version of the document of the pom.xml. + *

+ * 0 means that the loaded maven project has been loaded by a pom.xml file (it + * is not editing). + *

+ * + * @return the last checked version of the document of the pom.xml. + */ + public int getLastCheckedVersion() { + return lastCheckedVersion; + } +} \ No newline at end of file diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenLemminxExtension.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenLemminxExtension.java index 7484f05a..1b43fcfc 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenLemminxExtension.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenLemminxExtension.java @@ -34,6 +34,9 @@ import java.util.Optional; import java.util.Properties; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -91,6 +94,8 @@ import org.eclipse.lemminx.extensions.maven.searcher.RemoteCentralRepositorySearcher; import org.eclipse.lemminx.extensions.maven.utils.DOMUtils; import org.eclipse.lemminx.extensions.maven.utils.MavenParseUtils; +import org.eclipse.lemminx.services.IXMLDocumentProvider; +import org.eclipse.lemminx.services.IXMLValidationService; import org.eclipse.lemminx.services.extensions.IXMLExtension; import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant; @@ -119,7 +124,8 @@ public class MavenLemminxExtension implements IXMLExtension { private static final Logger LOGGER = Logger.getLogger(MavenLemminxExtension.class.getName()); private static final String MAVEN_XMLLS_EXTENSION_REALM_ID = MavenLemminxExtension.class.getName(); - + private static final long WAIT_SAFE_TIMEOUT_SECONDS = 10; + private XMLExtensionsRegistry currentRegistry; private MavenLemminxWorkspaceReader workspaceReader = new MavenLemminxWorkspaceReader(); @@ -147,6 +153,8 @@ public class MavenLemminxExtension implements IXMLExtension { // Thread which loads Maven component (plexus container, maven session, etc) which can take some time. private CompletableFuture mavenInitializer; + private IXMLDocumentProvider documentProvider; + private IXMLValidationService validationService; private ProgressSupport progressSupport; @@ -181,6 +189,8 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) { this.currentRegistry = registry; this.resolverExtensionManager = registry.getResolverExtensionManager(); this.progressSupport = registry.getProgressSupport(); + this.documentProvider = registry.getDocumentProvider(); + this.validationService = registry.getValidationService(); try { // Do not invoke getters the MavenLemminxExtension in participant constructors, // or that will trigger loading of plexus, Maven and so on even for non pom files @@ -228,14 +238,17 @@ private synchronized CompletableFuture getOrCreateMavenInitializer() { // while Maven component is initializing. if (mavenInitializer == null) { if (isUnitTestMode()) { + mavenInitializer = new CompletableFuture<>(); doInitialize(() -> {}); - mavenInitializer = CompletableFuture.completedFuture(null); + mavenInitializer.complete(null); } else mavenInitializer = CompletableFutures.computeAsync(cancelChecker -> { doInitialize(cancelChecker); return null; }); } + // Start Maven Project Cache + mavenInitializer.thenAccept(t -> cache.start()); return mavenInitializer; } @@ -294,7 +307,7 @@ private void doInitialize(CancelChecker cancelChecker) { MavenExecutionResult mavenResult = new DefaultMavenExecutionResult(); // TODO: MavenSession is deprecated. Investigate for alternative mavenSession = new MavenSession(container, repositorySystemSession, mavenRequest, mavenResult); - cache = new MavenProjectCache(mavenSession); + cache = new MavenProjectCache(mavenSession, documentProvider); // Step5 : create local repository searcher cancelChecker.checkCanceled(); @@ -473,6 +486,11 @@ private DefaultPlexusContainer newPlexusContainer() throws PlexusContainerExcept try { realm = classWorld.getRealm(MAVEN_XMLLS_EXTENSION_REALM_ID); } catch (NoSuchRealmException e) { + try { + classWorld.close(); + } catch (IOException ex) { + LOGGER.log(Level.SEVERE, ex.getMessage(), ex); + } throw new PlexusContainerException("Could not lookup required class realm", e); } final ContainerConfiguration mavenCoreCC = new DefaultContainerConfiguration() // @@ -525,8 +543,15 @@ public void stop(XMLExtensionsRegistry registry) { if (mavenInitializer != null) { mavenInitializer.cancel(true); } + cache.stop(); } + /** + * Checks if the specified document is to be treated as a Maven Project + * + * @param document A document to be checked + * @return true If the specified document is to be treated as a Maven Project + */ public static boolean match(DOMDocument document) { try { return match(new File(URI.create(document.getDocumentURI())).toPath()); @@ -537,56 +562,122 @@ public static boolean match(DOMDocument document) { } } + /** + * Checks if the specified file is to be treated as a Maven Project + * + * @param file A file to be checked + * @return true If the specified file is to be treated as a Maven Project + */ public static boolean match(Path file) { String fileName = file != null ? file.getFileName().toString() : null; return fileName != null && ((fileName.startsWith("pom") && fileName.endsWith(".xml")) || fileName.endsWith(Maven.POMv4) || fileName.endsWith(".pom")); } + /** + * Returns the instance of IXMLValidationService configured on the extension + * + * @return IXMLValidationService object + */ + public IXMLValidationService getValidationService() { + return validationService; + } + + /** + * Returns the cache of collected Maven Projects + * + * @return Maven Project Cache support + */ public MavenProjectCache getProjectCache() { initialize(); return this.cache; } + /** + * Returns the Maven Session object + * + * @return Maven Session object + */ public MavenSession getMavenSession() { initialize(); return this.mavenSession; } + /** + * Returns the Plexus Container object + * + * @return Plexus Container object + */ public PlexusContainer getPlexusContainer() { initialize(); return this.container; } + /** + * Returns the Maven Build Plugin Manager object + * + * @return Maven Build Plugin Manager object + */ public BuildPluginManager getBuildPluginManager() { initialize(); return buildPluginManager; } + /** + * Returns the Local Maven Repository Searcher + * + * @return Local Maven Repository Searcher + */ public LocalRepositorySearcher getLocalRepositorySearcher() { initialize(); return localRepositorySearcher; } + /** + * Returns the Maven Plugin Manager object + * + * @return Maven Plugin Manager object + */ public MavenPluginManager getMavenPluginManager() { initialize(); return mavenPluginManager; } + /** + * Returns the Remote Maven Searcher (uses Maven Search API) instance + * + * @return Optional RemoteCentralRepositorySearcher object + */ public Optional getCentralSearcher() { initialize(); return Optional.ofNullable(centralSearcher); } + /** + * Returns the URI Resolver Extension Manager + * + * @return URI Resolver Extension Manager object + */ public URIResolverExtensionManager getUriResolveExtentionManager() { initialize(); return resolverExtensionManager; } + /** + * Returns set of URI objects representing the folders available in Workspace + * + * @return A linkedHashSet of Workspace folders URI + */ public LinkedHashSet getCurrentWorkspaceFolders() { return currentWorkspaceFolders; } + /** + * A handler to be called on changing the Workspace Folders + * + * @param added An array of added folders + * @param removed An array of removed folders + */ public void didChangeWorkspaceFolders(URI[] added, URI[] removed) { CompletableFuture initializer = getMavenInitializer(); if (initializer.isDone()) { @@ -596,7 +687,7 @@ public void didChangeWorkspaceFolders(URI[] added, URI[] removed) { } } - public void internalDidChangeWorkspaceFolders(URI[] added, URI[] removed) { + private void internalDidChangeWorkspaceFolders(URI[] added, URI[] removed) { currentWorkspaceFolders.addAll(List.of(added != null? added : new URI[0])); currentWorkspaceFolders.removeAll(List.of(removed != null ? removed : new URI[0])); WorkspaceReader workspaceReader = mavenRequest.getWorkspaceReader(); @@ -765,12 +856,30 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO /** * Returns the list of Maven Projects currently added to the Workspace * + * @param wait A boolean 'true' indicates that all projects are to + * be returned, not only the cached ones at the moment, + * method should wait for the final build result, + * otherwise the only project that are already built and + * cached are to be returned, the rest of the projects + * are to be built in background * @return List of Maven Projects */ - public List getCurrentWorkspaceProjects() { + public List getCurrentWorkspaceProjects(boolean wait) { return workspaceReader.getCurrentWorkspaceArtifactFiles().stream() - .map(f -> getProjectCache().getLastSuccessfulMavenProject(f)) - .filter(Objects::nonNull).toList(); + .map(file -> { + try { + CompletableFuture loadedProject = + getProjectCache().getLoadedMavenProject(toUriASCIIString(file)); + return wait ? loadedProject.get(WAIT_SAFE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + : loadedProject.getNow(null); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return null; + } + }) + .filter(Objects::nonNull) + .map(LoadedMavenProject::getMavenProject) + .toList(); } /** @@ -830,4 +939,14 @@ public static boolean isUnitTestMode() { public static void setUnitTestMode(boolean unitTestMode) { MavenLemminxExtension.unitTestMode = unitTestMode; } + + /** + * Gets a normalized URI ASCII string from the given File + * + * @param file A File + * @return Normalized URI ASCII string + */ + public static String toUriASCIIString(File file) { + return file.toURI().normalize().toASCIIString(); + } } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java index 20e50197..35eb525a 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java @@ -8,18 +8,27 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.maven; +import static org.eclipse.lemminx.extensions.maven.utils.URIUtils.toURIKey; +import static org.eclipse.lemminx.extensions.maven.utils.URIUtils.toURIString; + +import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CancellationException; -import java.util.function.Consumer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -52,277 +61,496 @@ import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.extensions.maven.utils.DOMModelSource; +import org.eclipse.lemminx.services.IXMLDocumentProvider; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.CompletableFutures.FutureCancelChecker; public class MavenProjectCache { private static final Logger LOGGER = Logger.getLogger(MavenProjectCache.class.getName()); - private final Map projectCache; + private final Map projectCache; private final MavenSession mavenSession; + private final IXMLDocumentProvider documentProvider; - MavenXpp3Reader mavenReader = new MavenXpp3Reader(); - private ProjectBuilder projectBuilder; - - private final List> projectParsedListeners = new ArrayList<>(); + private ProjectBuildManager projectBuildManager; - public MavenProjectCache(MavenSession mavenSession) { + public MavenProjectCache(MavenSession mavenSession, IXMLDocumentProvider documentProvider) { this.mavenSession = mavenSession; this.projectCache = new HashMap<>(); - initializeMavenBuildState(); + this.documentProvider = documentProvider; + this.projectBuildManager = new ProjectBuildManager(); } /** - * Returns the last successfully parsed and cached Maven Project for the given - * document - * - * @param document A given Document - * @return the last MavenDocument that could be build for the more recent - * version of the provided document. If document fails to build a - * MavenProject, a former version will be returned. Can be - * null. + * Should be called when Maven Lemminx Extension initialization + * is successfully finished */ - public MavenProject getLastSuccessfulMavenProject(DOMDocument document) { - check(document); - LoadedMavenProject project = getLoadedMavenProject(document.getTextDocument().getUri()); - return project != null ? project.getMavenProject() : null; + public void start() { + projectBuildManager.start(); } - /** - * Returns the last successfully parsed and cached Maven Project for the given - * POM file - * - * @param pomFile A given POM File - * @return the last MavenDocument that could be build for the more recent - * version of the provided document. If document fails to build a - * MavenProject, a former version will be returned. Can be - * null. - */ - public MavenProject getLastSuccessfulMavenProject(File pomFile) { - check(pomFile); - LoadedMavenProject project = getLoadedMavenProject(pomFile.toURI()); - return project != null ? project.getMavenProject() : null; + public void stop() { + projectBuildManager.stop(); } + + class ProjectBuildManager { + private static final int CORE_POOL_SIZE = 10; - /** - * - * @param document - * @return the problems for the latest version of the document (either in cache, - * or the one passed in arguments) - */ - public LoadedMavenProject getLoadedMavenProject(DOMDocument document) { - check(document); - return getLoadedMavenProject(document.getTextDocument().getUri()); - } + private Map toProcess = new HashMap<>(); + private final PriorityBlockingQueue runnables = new PriorityBlockingQueue<>(1, PRIORITIZED_DEEPEST_FIRST); + private final ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 1, 0, TimeUnit.MILLISECONDS, runnables); + private final MavenXpp3Reader mavenReader = new MavenXpp3Reader(); + private ProjectBuilder projectBuilder; - private void check(DOMDocument document) { - LoadedMavenProject project = getLoadedMavenProject(document.getTextDocument().getUri()); - Integer last = project != null ? project.getLastCheckedVersion() : null; - if (last == null || last.intValue() < document.getTextDocument().getVersion()) { - parseAndCache(document); - } - } + private final class BuildProjectRunnable implements Runnable { + final String uri; + final FileModelSource source; + final CompletableFuture future; + private int priority; - private void check(File pomFile) { - LoadedMavenProject project = getLoadedMavenProject(pomFile.toURI()); - Integer last = project != null ? project.getLastCheckedVersion() : null; - if (last == null || last.intValue() < 0) { - parseAndCache(pomFile); - } - } + private BuildProjectRunnable(String uri, FileModelSource source) { + this.uri = uri; + this.source = source; + this.future = new CompletableFuture<>(); + this.priority = 0; + } + + int getPriority() { + return this.priority; + } + + void bumpPriority() { + this.priority++; + } - public Optional getSnapshotProject(File file) { - LoadedMavenProject loadedProject = getLoadedMavenProject(file.toURI()); - MavenProject lastKnownVersionMavenProject = loadedProject != null ? loadedProject.getMavenProject() : null; - if (lastKnownVersionMavenProject != null) { - return Optional.of(lastKnownVersionMavenProject); - } - try { - MavenProject project = projectBuilder.build(file, newProjectBuildingRequest()).getProject(); - return Optional.of(project); - } catch (ProjectBuildingException e) { - List result = e.getResults(); - if (result != null && result.size() == 1 && result.get(0).getProject() != null) { - MavenProject project = result.get(0).getProject(); - return Optional.of(project); + @Override + public void run() { + try { + future.complete(build(source, new FutureCancelChecker(future))); + } catch (Exception e) { // This should include CancellationException + future.completeExceptionally(e); + } } - } catch (Exception e) { - // Some other kinds of exceptions may be thrown, for instance, - // an IllegalStateException in case of fail to acquire write lock for an - // artifact - LOGGER.log(Level.SEVERE, e.getMessage(), e); - } - return Optional.empty(); - } + @Override + public boolean equals(Object obj) { + if (super.equals(obj)) { + return true; + } + if (obj instanceof BuildProjectRunnable runnable) { + return this.hashCode() == runnable.hashCode(); + } + return false; + } - private void parseAndCache(URI uri, int version, FileModelSource source) { - Collection problems = new ArrayList<>(); - DependencyResolutionResult dependencyResolutionResult = null; - uri = uri.normalize(); - final File file = new File(uri); - MavenProject project = null; - try { - ProjectBuildingResult buildResult = projectBuilder.build(source, newProjectBuildingRequest()); - project = buildResult.getProject(); - problems.addAll(buildResult.getProblems()); - dependencyResolutionResult = buildResult.getDependencyResolutionResult(); - - if (project != null) { - // setFile should ideally be invoked during project build, but related methods - // to pass modelSource and pomFile are private - project.setFile(new File(uri)); + @Override + public int hashCode() { + return Objects.hash(toURIKey(uri), source); } - } catch (ProjectBuildingException e) { - if (e.getResults() == null) { - if (e.getCause() instanceof ModelBuildingException modelBuildingException) { - // Try to manually build a minimal project from the document to collect - // lower-level - // errors and to have something usable in cache for most basic operations - modelBuildingException.getProblems().stream() - .filter(p -> !(p.getException() instanceof ModelParseException)).forEach(problems::add); - try (InputStream documentStream = source.getInputStream()) { - Model model = mavenReader.read(documentStream); - project = new MavenProject(model); - project.setRemoteArtifactRepositories(model.getRepositories().stream() - .map(repo -> new MavenArtifactRepository(repo.getId(), repo.getUrl(), - new DefaultRepositoryLayout(), - new ArtifactRepositoryPolicy(true, - ArtifactRepositoryPolicy.UPDATE_POLICY_INTERVAL, - ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN), - new ArtifactRepositoryPolicy(true, - ArtifactRepositoryPolicy.UPDATE_POLICY_INTERVAL, - ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN))) - .distinct().collect(Collectors.toList())); + + public LoadedMavenProject build(FileModelSource source, CancelChecker cancelChecker) { + Collection problems = new ArrayList<>(); + DependencyResolutionResult dependencyResolutionResult = null; + MavenProject project = null; + File file = source.getFile(); + try { + ProjectBuildingResult buildResult = projectBuilder.build(source, newProjectBuildingRequest()); + cancelChecker.checkCanceled(); + project = buildResult.getProject(); + problems.addAll(buildResult.getProblems()); + dependencyResolutionResult = buildResult.getDependencyResolutionResult(); + + if (project != null) { + // setFile should ideally be invoked during project build, but related methods + // to pass modelSource and pomFile are private project.setFile(file); - project.setBuild(new Build()); - } catch (XmlPullParserException parserException) { - // XML document is invalid fo parsing (eg user is typing), it's a valid state - // that shouldn't log - // exceptions - } catch (IOException ex) { - LOGGER.log(Level.SEVERE, e.getMessage(), e); } - } else { - problems.add( - new DefaultModelProblem(e.getMessage(), Severity.FATAL, Version.BASE, null, -1, -1, e)); - } - } else { - e.getResults().stream().flatMap(result -> result.getProblems().stream()).forEach(problems::add); - if (e.getResults().size() == 1) { - project = e.getResults().get(0).getProject(); - if (project != null) { - project.setFile(new File(uri)); + } catch (ProjectBuildingException e) { + if (e.getResults() == null) { + if (e.getCause() instanceof ModelBuildingException modelBuildingException) { + // Try to manually build a minimal project from the document to collect + // lower-level + // errors and to have something usable in cache for most basic operations + modelBuildingException.getProblems().stream() + .filter(p -> !(p.getException() instanceof ModelParseException)).forEach(problems::add); + try (InputStream documentStream = source.getInputStream()) { + Model model = mavenReader.read(documentStream); + cancelChecker.checkCanceled(); + project = new MavenProject(model); + project.setRemoteArtifactRepositories(model.getRepositories().stream() + .map(repo -> new MavenArtifactRepository(repo.getId(), repo.getUrl(), + new DefaultRepositoryLayout(), + new ArtifactRepositoryPolicy(true, + ArtifactRepositoryPolicy.UPDATE_POLICY_INTERVAL, + ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN), + new ArtifactRepositoryPolicy(true, + ArtifactRepositoryPolicy.UPDATE_POLICY_INTERVAL, + ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN))) + .distinct().collect(Collectors.toList())); + project.setFile(file); + project.setBuild(new Build()); + } catch (XmlPullParserException | EOFException parserException) { + // XML document is invalid for parsing (eg user is typing), it's a valid state + // that shouldn't log + // exceptions + } catch (IOException ex) { + // Log at low severity for debugging purposes + LOGGER.log(Level.FINER, e.getMessage(), e); + } + } else { + problems.add( + new DefaultModelProblem(e.getMessage(), Severity.FATAL, Version.BASE, null, -1, -1, e)); + } + } else { + e.getResults().stream().flatMap(result -> result.getProblems().stream()).forEach(problems::add); + if (e.getResults().size() == 1) { + project = e.getResults().get(0).getProject(); + if (project != null) { + project.setFile(file); + } + } } + } catch (CancellationException e) { + // The document which has been used to load Maven project is out of dated + throw e; + } catch (Exception e) { + // Do not add any info, like lastCheckedVersion or problems, to the cache + // In case of project/problems etc. is not available due to an exception + // happened. + // + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return null; // Nothing to be cached } + return new LoadedMavenProject(project, problems, dependencyResolutionResult); } - } catch (CancellationException e) { - // The document which has been used to load Maven project is out of dated - throw e; - } catch (Exception e) { - // Do not add any info, like lastCheckedVersion or problems, to the cache - // In case of project/problems etc. is not available due to an exception - // happened. - // - LOGGER.log(Level.SEVERE, e.getMessage(), e); - return; // Nothing to be cached } - cacheProject(project, file, uri, version, problems, dependencyResolutionResult); - } + private ProjectBuildManager() { + initializeMavenBuildState(); + } - private void cacheProject(final MavenProject project, File file, URI uri, int version, - Collection problems, DependencyResolutionResult dependencyResolutionResult) { - LoadedMavenProject loadedProject = new LoadedMavenProject(project, version, problems, - dependencyResolutionResult); - if (project != null) { - projectParsedListeners.forEach(listener -> listener.accept(project)); + private static final Object runnableKey(String uri, FileModelSource source) { + return Integer.valueOf(Objects.hash(uri, source)); + } + + private void initializeMavenBuildState() { + if (projectBuilder != null) { + return; + } + try { + projectBuilder = getPlexusContainer().lookup(ProjectBuilder.class); + System.setProperty(DefaultProjectBuilder.DISABLE_GLOBAL_MODEL_CACHE_SYSTEM_PROPERTY, + Boolean.toString(true)); + } catch (ComponentLookupException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + } + + private void start() { + if (executor.getCorePoolSize() == 0) { + executor.setCorePoolSize(CORE_POOL_SIZE); + } } - projectCache.put(uri, loadedProject); - } - private void parseAndCache(DOMDocument document) { - URI uri = URI.create(document.getDocumentURI()).normalize(); - int version = document.getTextDocument().getVersion(); - DOMModelSource source = new DOMModelSource(document); - parseAndCache(uri, version, source); - } + private void stop() { + if (executor.getCorePoolSize() > 0) { + executor.setCorePoolSize(0); + } + executor.shutdown(); + } - private void parseAndCache(File pomFile) { - URI uri = pomFile.toURI().normalize(); - FileModelSource source = new FileModelSource(pomFile); - parseAndCache(uri, 0, source); - } + private static final Comparator PRIORITIZED_DEEPEST_FIRST = (o1, o2) -> { + if (!(o1 instanceof BuildProjectRunnable r1 && o2 instanceof BuildProjectRunnable r2)) { + return 0; + } + int result = Comparator.comparingInt(BuildProjectRunnable::getPriority) + .compare(r1, r2); + if (result == 0) { + result = r1.uri.compareTo(r2.uri); + } + return result; + }; + + /** + * Asynchronously builds a provided document from a source provided + * + * @param uri An URI String identifying the document ro be built + * @param source A FIleModelSource for the document to be built + * @return A CompletableFuture of LoadedMavenProject object + */ + public CompletableFuture build(final String uri, final FileModelSource source) { + BuildProjectRunnable runnable = null; + Object key = runnableKey(toURIKey(uri), source); + synchronized (toProcess) { + runnable = toProcess.get(key); + if (runnable != null) { + // Project is already queued to be built, so just bump the + // runnable priority to force build to be started earlier. + runnable.bumpPriority(); + } else { + runnable = new BuildProjectRunnable(uri, source); + toProcess.put(key, runnable); + executor.execute(runnable); + runnable.future.whenComplete((ok, error) -> toProcess.remove(key)); + } + } + return runnable.future; + } - private ProjectBuildingRequest newProjectBuildingRequest() { - return newProjectBuildingRequest(true); - } + /** + * Returns a cached MavenProject object or builds a new one from a file source + * The resulting MavenProject is a snapshot that is not to be cached (unless it's + * taken from Maven Project Cache) + * + * @param file of Maven Project to be built + * @return Optional of MavenProject object + */ + public Optional getSnapshotProject(File file) { + LoadedMavenProjectProvider projectProvider = projectCache.get(toURIKey(file)); + Integer last = projectProvider != null ? projectProvider.getLastCheckedVersion() : null; + if (last != null && last.intValue() >= 0) { + LoadedMavenProject loadedProject = projectProvider.getLoadedMavenProject().getNow(null); + MavenProject project = loadedProject != null ? loadedProject.getMavenProject() : null; + if (project != null) { + return Optional.of(project); + } + } - private ProjectBuildingRequest newProjectBuildingRequest(boolean resolveDependencies) { - ProjectBuildingRequest request = new DefaultProjectBuildingRequest(); - MavenExecutionRequest mavenRequest = mavenSession.getRequest(); - request.setSystemProperties(mavenRequest.getSystemProperties()); - request.setLocalRepository(mavenRequest.getLocalRepository()); - request.setRemoteRepositories(mavenRequest.getRemoteRepositories()); - request.setPluginArtifactRepositories(mavenRequest.getPluginArtifactRepositories()); - // TODO more to transfer from mavenRequest to ProjectBuildingRequest? - request.setRepositorySession(mavenSession.getRepositorySession()); - request.setResolveDependencies(resolveDependencies); - - // See: https://issues.apache.org/jira/browse/MRESOLVER-374 - request.getUserProperties().setProperty("aether.syncContext.named.factory", "noop"); - return request; - } + try { + return Optional.of(projectBuilder.build(file, newProjectBuildingRequest()).getProject()); + } catch (ProjectBuildingException e) { + List result = e.getResults(); + if (result != null && result.size() == 1 && result.get(0).getProject() != null) { + return Optional.of(result.get(0).getProject()); + } + } catch (Exception e) { + // Some other kinds of exceptions may be thrown, for instance, + // an IllegalStateException in case of fail to acquire write lock for an + // artifact + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } - private void initializeMavenBuildState() { - if (projectBuilder != null) { - return; + return Optional.empty(); } - try { - projectBuilder = getPlexusContainer().lookup(ProjectBuilder.class); - System.setProperty(DefaultProjectBuilder.DISABLE_GLOBAL_MODEL_CACHE_SYSTEM_PROPERTY, - Boolean.toString(true)); - } catch (ComponentLookupException e) { - LOGGER.log(Level.SEVERE, e.getMessage(), e); + + /** + * Builds the MavenProject object from a Document being edited using a given Profile ID. + * The resulting MavenProject is a snapshot that is not to be cached + * + * @param document Maven Project DOMDocument to be built + * @param profileId A profile ID to be used on builf + * @param resolve a boolean indicating if dependencies are to be resolved during the build + * @return Optional of MavenProject object + */ + public MavenProject getSnapshotProject(DOMDocument document, String profileId, boolean resolve) { + // it would be nice to directly rebuild from Model instead of re-parsing text + ProjectBuildingRequest request = newProjectBuildingRequest(resolve); + if (profileId != null) { + request.setActiveProfileIds(List.of(profileId)); + } + try { + DOMModelSource source = new DOMModelSource(document); + return projectBuilder.build(source, request).getProject(); + } catch (CancellationException e) { + // The document which has been used to load Maven project is out of dated + throw e; + } catch (ProjectBuildingException e) { + List result = e.getResults(); + if (result != null && result.size() == 1 && result.get(0).getProject() != null) { + return result.get(0).getProject(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * Creates a new default Maven Project Building request (with dependency + * resolve enabled) + * + * @return A ProjectBuildingRequest object + */ + public ProjectBuildingRequest newProjectBuildingRequest() { + return newProjectBuildingRequest(true); + } + + /** + * Creates a new default Maven Project Building request + * + * @param resolveDependencies a boolean indicating if the dependency + * resolve is to be enabled on the request. + * + * @return A ProjectBuildingRequest object + */ + public ProjectBuildingRequest newProjectBuildingRequest(boolean resolveDependencies) { + ProjectBuildingRequest request = new DefaultProjectBuildingRequest(); + MavenExecutionRequest mavenRequest = mavenSession.getRequest(); + request.setSystemProperties(mavenRequest.getSystemProperties()); + request.setLocalRepository(mavenRequest.getLocalRepository()); + request.setRemoteRepositories(mavenRequest.getRemoteRepositories()); + request.setPluginArtifactRepositories(mavenRequest.getPluginArtifactRepositories()); + // TODO more to transfer from mavenRequest to ProjectBuildingRequest? + request.setRepositorySession(mavenSession.getRepositorySession()); + request.setResolveDependencies(resolveDependencies); + + // See: https://issues.apache.org/jira/browse/MRESOLVER-374 + request.getUserProperties().setProperty("aether.syncContext.named.factory", "noop"); + return request; } } + /** + * Returns the PlexusContainer object used in Maven Session + * + * @return A PlexusComtainer object + */ public PlexusContainer getPlexusContainer() { return mavenSession.getContainer(); } + /** + * Returns the list of built Maven Projects currently available in the + * Maven Project Cache + * + * @return A Collection of currently available Maven Projects + */ public Collection getProjects() { - return projectCache.values().stream().map(LoadedMavenProject::getMavenProject).toList(); + return projectCache.values().stream() + .map(LoadedMavenProjectProvider::getLoadedMavenProject) + .map(f -> f.getNow(null)).filter(Objects::nonNull) + .map(LoadedMavenProject::getMavenProject) + .toList(); } + /** + * Returns the last successfully parsed and cached Maven Project for the + * given POM file if exists or builds a Snapshot Maven Project (not caching + * the Maven Project build results, nor saving the build problems if any) + * + * @param file + * @return Optional Maven Project + */ + public Optional getSnapshotProject(File file) { + return projectBuildManager.getSnapshotProject(file); + } + + /** + * Returns the successfully parsed Maven Project built from the given + * document (not caching the Maven Project build results, nor saving + * the build problems if any) + * + * @param document + * @param profileId + * @return Optional Maven Project + */ public MavenProject getSnapshotProject(DOMDocument document, String profileId) { return getSnapshotProject(document, profileId, true); } - public MavenProject getSnapshotProject(DOMDocument document, String profileId, boolean resolveDependencies) { - // it would be nice to directly rebuild from Model instead of reparsing text - ProjectBuildingRequest request = newProjectBuildingRequest(resolveDependencies); - if (profileId != null) { - request.setActiveProfileIds(List.of(profileId)); + /** + * Returns the successfully parsed Maven Project built from the given + * document (not caching the Maven Project build results, nor saving + * the build problems if any) + * + * @param document + * @param profileId + * @param resolve + * @return Optional Maven Project + */ + public MavenProject getSnapshotProject(DOMDocument document, String profileId, boolean resolve) { + return projectBuildManager.getSnapshotProject(document, profileId, resolve); + } + + /** + * Returns the last successfully parsed and cached Maven Project for the + * given POM file if exists and up to date + * + * @param pomFile A given POM File + * @return the last MavenDocument that could be build for the more recent + * version of the provided document. If document fails to build a + * MavenProject, a former version will be returned. Can be + * null. + */ + public MavenProject getLastSuccessfulMavenProject(File pomFile) { + CompletableFuture project = getLoadedMavenProject(pomFile); + try { + return project != null ? project.get().getMavenProject() : null; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); } + return null; + } + + /** + * Returns the last successfully parsed and cached Maven Project for the given + * document + * + * @param document A given Document + * @return the last MavenDocument that could be build for the more recent + * version of the provided document. If document fails to build a + * MavenProject, a former version will be returned. Can be + * null. + */ + public MavenProject getLastSuccessfulMavenProject(DOMDocument document) { + CompletableFuture project = getLoadedMavenProject(document); try { - DOMModelSource source = new DOMModelSource(document); - return projectBuilder.build(source, request).getProject(); - } catch (CancellationException e) { - // The document which has been used to load Maven project is out of dated - throw e; - } catch (ProjectBuildingException e) { - List result = e.getResults(); - if (result != null && result.size() == 1 && result.get(0).getProject() != null) { - return result.get(0).getProject(); - } + return project != null ? project.get().getMavenProject() : null; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); } return null; } - private LoadedMavenProject getLoadedMavenProject(String uri) { - return getLoadedMavenProject(URI.create(uri)); + /** + * Returns a Completable Future of Loaded Maven Project for the given + * file + * + * @param file A POM file + * @return Completable Future of LoadedMavenDocument that could be build for + * the most recent version of the provided file. + */ + public CompletableFuture getLoadedMavenProject(File pomFile) { + return getLoadedMavenProject(toURIString(pomFile)); } - - private LoadedMavenProject getLoadedMavenProject(URI uri) { - return projectCache.get(uri.normalize()); + + /** + * Returns a Completable Future of Loaded Maven Project for the given + * document + * + * @param file A DOMDocument + * @return Completable Future of LoadedMavenDocument that could be build for + * the most recent version of the provided document. + */ + public CompletableFuture getLoadedMavenProject(DOMDocument document) { + return getLoadedMavenProject(document.getDocumentURI()); + } + + /** + * Returns a Completable Future of Loaded Maven Project for the given + * URI String document identifier + * + * @param uriString A document URI String + * @return Completable Future of LoadedMavenDocument that could be build for + * the most recent version of the document represented by the specified + * URI String identifier. + */ + public CompletableFuture getLoadedMavenProject(String uriString) { + String uriKey = toURIKey(uriString); + LoadedMavenProjectProvider provider = projectCache.get(uriKey); + if (provider == null) { + synchronized (projectCache) { + provider = projectCache.get(uriKey); + if (provider == null) { + provider = new LoadedMavenProjectProvider(uriString, documentProvider, projectBuildManager); + projectCache.put(uriKey, provider); + } + } + } + return provider.getLoadedMavenProject(); } } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipant.java index f2c31814..2eb09c98 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipant.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipant.java @@ -131,12 +131,9 @@ import org.w3c.dom.NodeList; public class MavenCompletionParticipant extends CompletionParticipantAdapter { - - public static final String WAITING_LABEL = "⏳ Waiting for Maven Central Response..."; - private static final Logger LOGGER = Logger.getLogger(MavenCompletionParticipant.class.getName()); - private static final Pattern ARTIFACT_ID_PATTERN = Pattern.compile("[-.a-zA-Z0-9]+"); + private static final Pattern ARTIFACT_ID_PATTERN = Pattern.compile("[-.a-zA-Z0-9]+"); private static final String FILE_TYPE = "File"; private static final String STRING_TYPE = "File"; private static final String DIRECTORY_STRING_LC = "directory"; @@ -183,10 +180,12 @@ public void onTagOpen(ICompletionRequest request, ICompletionResponse response, return; } + cancelChecker.checkCanceled(); try { DOMNode tag = request.getNode(); if (DOMUtils.isADescendantOf(tag, CONFIGURATION_ELT)) { - collectPluginConfiguration(request).forEach(response::addCompletionItem); + collectPluginConfiguration(request, cancelChecker).forEach(response::addCompletionItem); + cancelChecker.checkCanceled(); } } catch (MavenInitializationException | MavenModelOutOfDatedException e) { // - Maven is initializing @@ -196,53 +195,66 @@ public void onTagOpen(ICompletionRequest request, ICompletionResponse response, } // TODO: Factor this with MavenHoverParticipant's equivalent method - private List collectPluginConfiguration(ICompletionRequest request) { + private List collectPluginConfiguration(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); boolean supportsMarkdown = request.canSupportMarkupKind(MarkupKind.MARKDOWN); try { - Set parameters = MavenPluginUtils.collectPluginConfigurationMojoParameters(request, plugin); - + Set parameters = MavenPluginUtils.collectPluginConfigurationMojoParameters(request, plugin, cancelChecker); if (CONFIGURATION_ELT.equals(request.getParentElement().getLocalName())) { // The configuration element being completed is at the top level - return parameters.stream() + cancelChecker.checkCanceled(); + var result = parameters.stream() .map(param -> toTag(param.name, MavenPluginUtils.getMarkupDescription(param, null, supportsMarkdown), request)) .collect(Collectors.toList()); - } + cancelChecker.checkCanceled(); + return result; + } // Nested case: node is a grand child of configuration // Get the node's ancestor which is a child of configuration + cancelChecker.checkCanceled(); DOMNode parentParameterNode = DOMUtils.findAncestorThatIsAChildOf(request, CONFIGURATION_ELT); if (parentParameterNode != null) { + cancelChecker.checkCanceled(); List parentParameters = parameters.stream() .filter(mojoParameter -> mojoParameter.name.equals(parentParameterNode.getLocalName())) .collect(Collectors.toList()); if (!parentParameters.isEmpty()) { + cancelChecker.checkCanceled(); MojoParameter parentParameter = parentParameters.get(0); - if (parentParameter.getNestedParameters().size() == 1) { // The parent parameter must be a collection of a type MojoParameter nestedParameter = parentParameter.getNestedParameters().get(0); Class potentialInlineType = PlexusConfigHelper.getRawType(nestedParameter.getParamType()); if (potentialInlineType != null && PlexusConfigHelper.isInline(potentialInlineType)) { - return Collections.singletonList(toTag(nestedParameter.name, MavenPluginUtils + cancelChecker.checkCanceled(); + var result = Collections.singletonList(toTag(nestedParameter.name, MavenPluginUtils .getMarkupDescription(nestedParameter, parentParameter, supportsMarkdown), request)); + cancelChecker.checkCanceled(); + return result; } } // Get all deeply nested parameters + cancelChecker.checkCanceled(); List nestedParameters = parentParameter.getFlattenedNestedParameters(); nestedParameters.add(parentParameter); - return nestedParameters.stream() + cancelChecker.checkCanceled(); + var result = nestedParameters.stream() .map(param -> toTag(param.name, MavenPluginUtils.getMarkupDescription(param, parentParameter, supportsMarkdown), request)) .collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } } } catch (PluginResolutionException | PluginDescriptorParsingException | InvalidPluginDescriptorException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } + cancelChecker.checkCanceled(); return Collections.emptyList(); } @@ -252,12 +264,14 @@ public void onXMLContent(ICompletionRequest request, ICompletionResponse respons return; } + cancelChecker.checkCanceled(); try { if (request.getXMLDocument().getText().length() < 2) { - response.addCompletionItem(createMinimalPOMCompletionSnippet(request)); + response.addCompletionItem(createMinimalPOMCompletionSnippet(request, cancelChecker)); } DOMElement parent = request.getParentElement(); if (parent == null || parent.getLocalName() == null) { + cancelChecker.checkCanceled(); return; } DOMElement grandParent = parent.getParentElement(); @@ -276,52 +290,50 @@ public void onXMLContent(ICompletionRequest request, ICompletionResponse respons GAVInsertionStrategy gavInsertionStrategy = computeGAVInsertionStrategy(request); List allArtifactInfos = Collections.synchronizedList(new ArrayList<>()); LinkedHashMap nonArtifactCollector = new LinkedHashMap<>(); + cancelChecker.checkCanceled(); switch (parent.getLocalName()) { case SCOPE_ELT: collectSimpleCompletionItems(Arrays.asList(DependencyScope.values()), DependencyScope::getName, - DependencyScope::getDescription, request).forEach(response::addCompletionAttribute); + DependencyScope::getDescription, request, cancelChecker).forEach(response::addCompletionAttribute); break; case PHASE_ELT: collectSimpleCompletionItems(Arrays.asList(Phase.ALL_STANDARD_PHASES), phase -> phase.id, - phase -> phase.description, request).forEach(response::addCompletionAttribute); + phase -> phase.description, request, cancelChecker).forEach(response::addCompletionAttribute); + cancelChecker.checkCanceled(); break; case GROUP_ID_ELT: if (isParentDeclaration) { - Optional filesystem = computeFilesystemParent(request); + Optional filesystem = computeFilesystemParent(request, cancelChecker); if (filesystem.isPresent()) { filesystem.map(MavenProject::getGroupId) .map(g -> toCompletionItem(g.toString(), null, request.getReplaceRange())) .filter(completionItem -> !nonArtifactCollector.containsKey(completionItem.getLabel())) .ifPresent(completionItem -> nonArtifactCollector.put(completionItem.getLabel(), completionItem)); } - // TODO localRepo - // TODO remoteRepos } else { // TODO if artifactId is set and match existing content, suggest only matching // groupId collectSimpleCompletionItems( isPlugin ? plugin.getLocalRepositorySearcher().searchPluginGroupIds() : plugin.getLocalRepositorySearcher().searchGroupIds(), - Function.identity(), Function.identity(), request).stream() + Function.identity(), Function.identity(), request, cancelChecker).stream() .filter(completionItem -> !nonArtifactCollector.containsKey(completionItem.getLabel())) .forEach(completionItem -> nonArtifactCollector.put(completionItem.getLabel(), completionItem)); internalCollectRemoteGAVCompletion(request, isPlugin, allArtifactInfos, nonArtifactCollector, cancelChecker); } - internalCollectWorkspaceArtifacts(request, allArtifactInfos, nonArtifactCollector, groupId, artifactId); + internalCollectWorkspaceArtifacts(request, allArtifactInfos, nonArtifactCollector, groupId, artifactId, cancelChecker); // Sort and move nonArtifactCollector items to the response and clear nonArtifactCollector - nonArtifactCollector.entrySet().stream() - .map(entry -> entry.getValue()).forEach(response::addCompletionItem); + nonArtifactCollector.entrySet().stream().map(entry -> entry.getValue()) + .forEach(response::addCompletionItem); nonArtifactCollector.clear(); break; case ARTIFACT_ID_ELT: if (isParentDeclaration) { - Optional filesystem = computeFilesystemParent(request); + Optional filesystem = computeFilesystemParent(request, cancelChecker); if (filesystem.isPresent()) { filesystem.map(ArtifactWithDescription::new).ifPresent(allArtifactInfos::add); } - // TODO localRepo - // TODO remoteRepos } else { allArtifactInfos.addAll((isPlugin ? plugin.getLocalRepositorySearcher().getLocalPluginArtifacts() : plugin.getLocalRepositorySearcher().getLocalArtifactsLastVersion()).stream() @@ -330,19 +342,17 @@ public void onXMLContent(ICompletionRequest request, ICompletionResponse respons .map(ArtifactWithDescription::new).collect(Collectors.toList())); internalCollectRemoteGAVCompletion(request, isPlugin, allArtifactInfos, nonArtifactCollector, cancelChecker); } - internalCollectWorkspaceArtifacts(request, allArtifactInfos, nonArtifactCollector, groupId, artifactId); + internalCollectWorkspaceArtifacts(request, allArtifactInfos, nonArtifactCollector, groupId, artifactId, cancelChecker); break; case VERSION_ELT: if (isParentDeclaration) { - Optional filesystem = computeFilesystemParent(request); + Optional filesystem = computeFilesystemParent(request, cancelChecker); if (filesystem.isPresent()) { filesystem.map(MavenProject::getVersion).map(DefaultArtifactVersion::new) .map(version -> toCompletionItem(version.toString(), null, request.getReplaceRange())) .filter(completionItem -> !nonArtifactCollector.containsKey(completionItem.getLabel())) .ifPresent(completionItem -> nonArtifactCollector.put(completionItem.getLabel(), completionItem)); } - // TODO localRepo - // TODO remoteRepos } else { if (artifactId.isPresent()) { plugin.getLocalRepositorySearcher().getLocalArtifactsLastVersion().stream() @@ -355,11 +365,12 @@ public void onXMLContent(ICompletionRequest request, ICompletionResponse respons internalCollectRemoteGAVCompletion(request, isPlugin, allArtifactInfos, nonArtifactCollector, cancelChecker); } } - internalCollectWorkspaceArtifacts(request, allArtifactInfos, nonArtifactCollector, groupId, artifactId); + internalCollectWorkspaceArtifacts(request, allArtifactInfos, nonArtifactCollector, groupId, artifactId, cancelChecker); if (nonArtifactCollector.isEmpty()) { - response.addCompletionItem(toTextCompletionItem(request, "-SNAPSHOT")); + response.addCompletionItem(toTextCompletionItem(request, "-SNAPSHOT", cancelChecker)); } else { + cancelChecker.checkCanceled(); // Sort and move nonArtifactCollector items to the response and clear nonArtifactCollector final AtomicInteger sortIndex = new AtomicInteger(0); nonArtifactCollector.entrySet().stream() @@ -382,7 +393,7 @@ public int compare(CompletionItem o1, CompletionItem o2) { } break; case MODULE_ELT: - collectSubModuleCompletion(request).forEach(response::addCompletionItem); + collectSubModuleCompletion(request, cancelChecker).forEach(response::addCompletionItem); break; case TARGET_PATH_ELT: case DIRECTORY_ELT: @@ -391,25 +402,26 @@ public int compare(CompletionItem o1, CompletionItem o2) { case TEST_SOURCE_DIRECTORY_ELT: case OUTPUT_DIRECTORY_ELT: case TEST_OUTPUT_DIRECTORY_ELT: - collectRelativeDirectoryPathCompletion(request).forEach(response::addCompletionItem); + collectRelativeDirectoryPathCompletion(request, cancelChecker).forEach(response::addCompletionItem); break; case FILTER_ELT: if (FILTERS_ELT.equals(grandParent.getLocalName())) { - collectRelativeFilterPathCompletion(request).forEach(response::addCompletionItem); + collectRelativeFilterPathCompletion(request, cancelChecker).forEach(response::addCompletionItem); } break; case EXISTS_ELT: case MISSING_ELT: if (FILE_ELT.equals(grandParent.getLocalName())) { - collectRelativeAnyPathCompletion(request).forEach(response::addCompletionItem); + collectRelativeAnyPathCompletion(request, cancelChecker).forEach(response::addCompletionItem); } break; case RELATIVE_PATH_ELT: - collectRelativePathCompletion(request).forEach(response::addCompletionItem); + collectRelativePathCompletion(request, cancelChecker).forEach(response::addCompletionItem); break; case DEPENDENCIES_ELT: case DEPENDENCY_ELT: // TODO completion/resolve to get description for local artifacts + cancelChecker.checkCanceled(); allArtifactInfos.addAll(plugin.getLocalRepositorySearcher().getLocalArtifactsLastVersion().stream() .map(ArtifactWithDescription::new).collect(Collectors.toList())); internalCollectRemoteGAVCompletion(request, false, allArtifactInfos, nonArtifactCollector, cancelChecker); @@ -417,12 +429,13 @@ public int compare(CompletionItem o1, CompletionItem o2) { case PLUGINS_ELT: case PLUGIN_ELT: // TODO completion/resolve to get description for local artifacts + cancelChecker.checkCanceled(); allArtifactInfos.addAll(plugin.getLocalRepositorySearcher().getLocalPluginArtifacts().stream() .map(ArtifactWithDescription::new).collect(Collectors.toList())); internalCollectRemoteGAVCompletion(request, true, allArtifactInfos, nonArtifactCollector, cancelChecker); break; case PARENT_ELT: - Optional filesystem = computeFilesystemParent(request); + Optional filesystem = computeFilesystemParent(request, cancelChecker); if (filesystem.isPresent()) { filesystem.map(ArtifactWithDescription::new).ifPresent(allArtifactInfos::add); } else { @@ -431,35 +444,37 @@ public int compare(CompletionItem o1, CompletionItem o2) { } break; case GOAL_ELT: - collectGoals(request).forEach(response::addCompletionItem); + collectGoals(request, cancelChecker).forEach(response::addCompletionItem); break; case PACKAGING_ELT: - collectPackaging(request).forEach(response::addCompletionItem); + collectPackaging(request, cancelChecker).forEach(response::addCompletionItem); break; default: - Set parameters = MavenPluginUtils.collectPluginConfigurationMojoParameters(request, plugin) + Set parameters = MavenPluginUtils.collectPluginConfigurationMojoParameters(request, plugin, cancelChecker) .stream().filter(p -> p.name.equals(parent.getLocalName())) .filter(p -> (p.type.startsWith(FILE_TYPE)) || (p.type.startsWith(STRING_TYPE) && p.name.toLowerCase().endsWith(DIRECTORY_STRING_LC))) .collect(Collectors.toSet()); if (parameters != null && parameters.size() > 0) { - collectMojoParametersDefaultCompletion(request, parameters) - .forEach(response::addCompletionItem); + collectMojoParametersDefaultCompletion(request, parameters, cancelChecker) + .forEach(response::addCompletionItem); if (parameters.stream() .anyMatch(p -> !p.name.toLowerCase().endsWith(DIRECTORY_STRING_LC))) { // Show all files - collectRelativeAnyPathCompletion(request).forEach(response::addCompletionItem); + collectRelativeAnyPathCompletion(request, cancelChecker).forEach(response::addCompletionItem); } else { // Show only directories - collectRelativeDirectoryPathCompletion(request).forEach(response::addCompletionItem); + collectRelativeDirectoryPathCompletion(request, cancelChecker).forEach(response::addCompletionItem); } } - } + } + if (!allArtifactInfos.isEmpty()) { // As artifact list can be very big (around 4000 artifacts), to keep good performance, we filter it before sending to the LSP client // by checking that artifact id contains one of character of the computed completion prefix. // ex : org| will filter if the artifact contains 'o', 'r', or 'g' + cancelChecker.checkCanceled(); // 1. extract the completion prefix. char prefix[] = null; TextDocument textDocument = request.getXMLDocument().getTextDocument(); @@ -471,29 +486,32 @@ public int compare(CompletionItem o1, CompletionItem o2) { } final char completionPrefix[] = prefix; + cancelChecker.checkCanceled(); // 2. loop for each collected artifact by checking that artifact id matches the completion prefix. Comparator artifactInfoComparator = Comparator .comparing(artifact -> new DefaultArtifactVersion(artifact.artifact.getVersion())); final Comparator highestVersionWithDescriptionComparator = artifactInfoComparator .thenComparing( artifactInfo -> artifactInfo.description != null ? artifactInfo.description : ""); + cancelChecker.checkCanceled(); allArtifactInfos.stream() .collect(Collectors.groupingBy(artifact -> artifact.artifact.getGroupId() + ":" + artifact.artifact.getArtifactId())) .values().stream() .map(artifacts -> Collections.max(artifacts, highestVersionWithDescriptionComparator)) .filter(artifactInfo -> isMatchCompletionPrefix(artifactInfo.artifact.getArtifactId(), completionPrefix)) - .map(artifactInfo -> toGAVCompletionItem(artifactInfo, request, replaceRange, gavInsertionStrategy)) + .map(artifactInfo -> toGAVCompletionItem(artifactInfo, request, replaceRange, gavInsertionStrategy, cancelChecker)) .filter(completionItem -> !response.hasAttribute(completionItem.getLabel())) .forEach(response::addCompletionItem); } if (request.getNode().isText()) { - completeProperties(request).forEach(response::addCompletionAttribute); + completeProperties(request, cancelChecker).forEach(response::addCompletionAttribute); } } catch (MavenInitializationException | MavenModelOutOfDatedException e) { // - Maven is initializing // - or parse of maven model with DOM document is out of dated // -> catch the error to avoid breaking XML completion from LemMinX } + cancelChecker.checkCanceled(); } private static boolean isMatchCompletionPrefix(String completionItemText, char[] completionPrefix) { @@ -508,7 +526,8 @@ private static boolean isMatchCompletionPrefix(String completionItemText, char[] return false; } - private CompletionItem toTextCompletionItem(ICompletionRequest request, String text) throws BadLocationException { + private CompletionItem toTextCompletionItem(ICompletionRequest request, String text, CancelChecker cancelChecker) throws BadLocationException { + cancelChecker.checkCanceled(); CompletionItem res = new CompletionItem(text); res.setFilterText(text); TextEdit edit = new TextEdit(); @@ -517,6 +536,7 @@ private CompletionItem toTextCompletionItem(ICompletionRequest request, String t Position endOffset = request.getXMLDocument().positionAt(request.getOffset()); for (int startOffset = Math.max(0, request.getOffset() - text.length()); startOffset <= request .getOffset(); startOffset++) { + cancelChecker.checkCanceled(); String prefix = request.getXMLDocument().getText().substring(startOffset, request.getOffset()); if (text.startsWith(prefix)) { edit.setRange(new Range(request.getXMLDocument().positionAt(startOffset), endOffset)); @@ -524,6 +544,7 @@ private CompletionItem toTextCompletionItem(ICompletionRequest request, String t } } edit.setRange(new Range(endOffset, endOffset)); + cancelChecker.checkCanceled(); return res; } @@ -543,7 +564,8 @@ private GAVInsertionStrategy computeGAVInsertionStrategy(ICompletionRequest requ } @SuppressWarnings("deprecation") - private Optional computeFilesystemParent(ICompletionRequest request) { + private Optional computeFilesystemParent(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); Optional relativePath = null; if (request.getParentElement().getLocalName().equals(PARENT_ELT)) { relativePath = DOMUtils.findChildElementText(request.getNode(), RELATIVE_PATH_ELT); @@ -551,23 +573,29 @@ private Optional computeFilesystemParent(ICompletionRequest reques relativePath = DOMUtils.findChildElementText(request.getParentElement().getParentElement(), RELATIVE_PATH_ELT); } + cancelChecker.checkCanceled(); if (!relativePath.isPresent()) { relativePath = Optional.of(".."); } File referencedTargetPomFile = new File( new File(URI.create(request.getXMLDocument().getTextDocument().getUri())).getParentFile(), relativePath.orElse("")); + cancelChecker.checkCanceled(); if (referencedTargetPomFile.isDirectory()) { referencedTargetPomFile = new File(referencedTargetPomFile, Maven.POMv4); } if (referencedTargetPomFile.isFile()) { - return plugin.getProjectCache().getSnapshotProject(referencedTargetPomFile); + Optional project = plugin.getProjectCache().getSnapshotProject(referencedTargetPomFile); + cancelChecker.checkCanceled(); + return project; } + cancelChecker.checkCanceled(); return Optional.empty(); } - private CompletionItem createMinimalPOMCompletionSnippet(ICompletionRequest request) + private CompletionItem createMinimalPOMCompletionSnippet(ICompletionRequest request, CancelChecker cancelChecker) throws IOException, BadLocationException { + cancelChecker.checkCanceled(); CompletionItem item = new CompletionItem("minimal pom content"); item.setKind(CompletionItemKind.Snippet); item.setInsertTextFormat(InsertTextFormat.Snippet); @@ -575,31 +603,41 @@ private CompletionItem createMinimalPOMCompletionSnippet(ICompletionRequest requ model.setModelVersion("4.0.0"); model.setArtifactId( new File(URI.create(request.getXMLDocument().getTextDocument().getUri())).getParentFile().getName()); + cancelChecker.checkCanceled(); MavenXpp3Writer writer = new MavenXpp3Writer(); try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { + cancelChecker.checkCanceled(); writer.write(stream, model); + cancelChecker.checkCanceled(); TextEdit textEdit = new TextEdit( new Range(new Position(0, 0), request.getXMLDocument().positionAt(request.getXMLDocument().getText().length())), new String(stream.toByteArray())); item.setTextEdit(Either.forLeft(textEdit)); } + cancelChecker.checkCanceled(); return item; } - private Collection collectGoals(ICompletionRequest request) { + private Collection collectGoals(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); PluginDescriptor pluginDescriptor; try { pluginDescriptor = MavenPluginUtils.getContainingPluginDescriptor(request.getNode(), plugin); - return collectSimpleCompletionItems(pluginDescriptor.getMojos(), MojoDescriptor::getGoal, - MojoDescriptor::getDescription, request); + cancelChecker.checkCanceled(); + var result = collectSimpleCompletionItems(pluginDescriptor.getMojos(), MojoDescriptor::getGoal, + MojoDescriptor::getDescription, request, cancelChecker); + cancelChecker.checkCanceled(); + return result; } catch (PluginResolutionException | PluginDescriptorParsingException | InvalidPluginDescriptorException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } + cancelChecker.checkCanceled(); return Collections.emptySet(); } - private Collection collectPackaging(ICompletionRequest request) { + private Collection collectPackaging(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); Set packagingTypes = new LinkedHashSet<>(); packagingTypes.add(PACKAGING_TYPE_JAR); packagingTypes.add(PACKAGING_TYPE_WAR); @@ -609,36 +647,43 @@ private Collection collectPackaging(ICompletionRequest request) packagingTypes.add(PACKAGING_TYPE_MAVEN_PLUGIN); // dynamically load available packaging types from build plugins - updateAvailablePackagingTypes(packagingTypes, request); + updateAvailablePackagingTypes(packagingTypes, request, cancelChecker); - return packagingTypes.stream().map(type -> { - try { - CompletionItem item = toTextCompletionItem(request, type); - item.setDocumentation("Packagng Type: " + (type != null ? type : "unknown")); - item.setKind(CompletionItemKind.Value); - item.setSortText(type != null ? type : "zzz"); - return item; - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, e.getMessage(), e); - return toErrorCompletionItem(e); - } - }).collect(Collectors.toList()); + var result = packagingTypes.stream().map(type -> { + try { + cancelChecker.checkCanceled(); + CompletionItem item = toTextCompletionItem(request, type, cancelChecker); + item.setDocumentation("Packagng Type: " + (type != null ? type : "unknown")); + item.setKind(CompletionItemKind.Value); + item.setSortText(type != null ? type : "zzz"); + return item; + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return toErrorCompletionItem(e); + } + }).collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } - private void updateAvailablePackagingTypes(Set packagingTypes, ICompletionRequest request) { + private void updateAvailablePackagingTypes(Set packagingTypes, ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); MavenProject project = plugin.getProjectCache().getSnapshotProject(request.getXMLDocument(), null, false); if (project == null) { + cancelChecker.checkCanceled(); return; } for (Plugin plugin : project.getBuildPlugins()) { + cancelChecker.checkCanceled(); if (plugin.isExtensions()) { Artifact artifact = new DefaultArtifact( plugin.getGroupId(), plugin.getArtifactId(), null, plugin.getVersion()); - addPluginPackagingTypes(packagingTypes, artifact); + addPluginPackagingTypes(packagingTypes, artifact, cancelChecker); } } + cancelChecker.checkCanceled(); } /** @@ -652,23 +697,29 @@ private void updateAvailablePackagingTypes(Set packagingTypes, ICompleti * is assumed that there is something wrong with the user's project or * repository setup which prevents this method from completing. */ - private void addPluginPackagingTypes(Set packagingTypes, Artifact artifact) { + private void addPluginPackagingTypes(Set packagingTypes, Artifact artifact, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); File artifactPomFile = plugin.getLocalRepositorySearcher().findLocalFile(artifact); if (artifactPomFile == null) { + cancelChecker.checkCanceled(); return; } + cancelChecker.checkCanceled(); File artifactJarFile = new File(artifactPomFile.getParentFile().getAbsoluteFile(), artifact.getArtifactId() + '-' + artifact.getVersion() + JAR_EXT); try (JarFile jarFile = new JarFile(artifactJarFile.getAbsoluteFile())) { DocumentBuilder db = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder(); JarEntry componentsxml = jarFile.getJarEntry(COMPONENTS_PATH); + cancelChecker.checkCanceled(); if (componentsxml != null) { Document doc = db.parse(jarFile.getInputStream(componentsxml)); + cancelChecker.checkCanceled(); if (doc.getDocumentElement() != null) { doc.getDocumentElement().normalize(); NodeList components = doc.getElementsByTagName(COMPONENTS_COMPONENT_ELT); for (int i = 0; i < components.getLength(); i++) { + cancelChecker.checkCanceled(); Node component = components.item(i); if (component.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) component; @@ -688,10 +739,12 @@ private void addPluginPackagingTypes(Set packagingTypes, Artifact artifa } catch (Exception e) { // Broken XML, file not found, etc. Can't add packaging types. } + cancelChecker.checkCanceled(); } - private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, ICompletionRequest request,Range replaceRange, - GAVInsertionStrategy strategy) { + private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, ICompletionRequest request, + Range replaceRange, GAVInsertionStrategy strategy, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); boolean hasGroupIdSet = DOMUtils.findChildElementText(request.getParentElement().getParentElement(), GROUP_ID_ELT).isPresent() || DOMUtils.findChildElementText(request.getParentElement(), GROUP_ID_ELT).isPresent(); boolean insertArtifactIsEnd = !request.getParentElement().hasEndTag(); @@ -704,10 +757,12 @@ private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, item.setDocumentation(artifactInfo.description); } + cancelChecker.checkCanceled(); TextEdit textEdit = new TextEdit(); item.setTextEdit(Either.forLeft(textEdit)); textEdit.setRange(replaceRange); if (strategy == GAVInsertionStrategy.ELEMENT_VALUE_AND_SIBLING) { + cancelChecker.checkCanceled(); item.setKind(CompletionItemKind.Value); switch (request.getParentElement().getLocalName()) { case ARTIFACT_ID_ELT: @@ -720,6 +775,7 @@ private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, + (insertArtifactIsEnd ? "" : "")); item.setFilterText(artifactInfo.artifact.getArtifactId()); List additionalEdits = new ArrayList<>(2); + cancelChecker.checkCanceled(); if (insertGroupId) { Position insertionPosition; try { @@ -737,6 +793,7 @@ private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, LOGGER.log(Level.SEVERE, e.getMessage(), e); } } + cancelChecker.checkCanceled(); if (insertVersion) { Position insertionPosition; try { @@ -755,20 +812,25 @@ private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, LOGGER.log(Level.SEVERE, e.getMessage(), e); } } + cancelChecker.checkCanceled(); if (!additionalEdits.isEmpty()) { item.setAdditionalTextEdits(additionalEdits); } + cancelChecker.checkCanceled(); return item; case GROUP_ID_ELT: item.setLabel(artifactInfo.artifact.getGroupId()); textEdit.setNewText(artifactInfo.artifact.getGroupId()); + cancelChecker.checkCanceled(); return item; case VERSION_ELT: item.setLabel(artifactInfo.artifact.getVersion()); textEdit.setNewText(artifactInfo.artifact.getVersion()); + cancelChecker.checkCanceled(); return item; } } else { + cancelChecker.checkCanceled(); item.setLabel(artifactInfo.artifact.getArtifactId() + " - " + artifactInfo.artifact.getGroupId() + ":" + artifactInfo.artifact.getArtifactId() + ":" + artifactInfo.artifact.getVersion()); item.setKind(CompletionItemKind.Struct); @@ -781,17 +843,20 @@ private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, : ""; String oneLevelIndent = DOMUtils.getOneLevelIndent(request); String lineDelimiter = request.getLineIndentInfo().getLineDelimiter(); + cancelChecker.checkCanceled(); if (strategy instanceof GAVInsertionStrategy.NodeWithChildrenInsertionStrategy nodeWithChildren) { String elementName = nodeWithChildren.elementName; newText += "<" + elementName + ">" + lineDelimiter + gavElementsIndent + oneLevelIndent; suffix = lineDelimiter + gavElementsIndent + ""; gavElementsIndent += oneLevelIndent; } + cancelChecker.checkCanceled(); if (insertGroupId) { newText += "" + artifactInfo.artifact.getGroupId() + "" + lineDelimiter + gavElementsIndent; } newText += "" + artifactInfo.artifact.getArtifactId() + ""; + cancelChecker.checkCanceled(); if (insertVersion) { newText += lineDelimiter + gavElementsIndent; newText += "" + artifactInfo.artifact.getVersion() + ""; @@ -803,6 +868,7 @@ private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, return null; } } + cancelChecker.checkCanceled(); return item; } @@ -820,32 +886,36 @@ private static CompletionItem toTag(String name, MarkupContent description, ICom return res; } - private Collection completeProperties(ICompletionRequest request) { + private Collection completeProperties(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); MavenProject project = plugin.getProjectCache().getLastSuccessfulMavenProject(request.getXMLDocument()); if (project == null) { + cancelChecker.checkCanceled(); return Collections.emptySet(); } Map allProps = ParticipantUtils.getMavenProjectProperties(project); - - return allProps.entrySet().stream().map(property -> { - try { - CompletionItem item = toTextCompletionItem(request, "${" + property.getKey() + '}'); - item.setDocumentation( - "Default Value: " + (property.getValue() != null ? property.getValue() : "unknown")); - item.setKind(CompletionItemKind.Property); - // '$' sorts before alphabet characters, so we add z to make it appear later in - // proposals - item.setSortText("z${" + property.getKey() + "}"); - if (property.getKey().contains("env.")) { - // We don't want environment variables at the top of the completion proposals - item.setSortText("z${zzz" + property.getKey() + "}"); + cancelChecker.checkCanceled(); + var result = allProps.entrySet().stream().map(property -> { + try { + CompletionItem item = toTextCompletionItem(request, "${" + property.getKey() + '}', cancelChecker); + item.setDocumentation( + "Default Value: " + (property.getValue() != null ? property.getValue() : "unknown")); + item.setKind(CompletionItemKind.Property); + // '$' sorts before alphabet characters, so we add z to make it appear later in + // proposals + item.setSortText("z${" + property.getKey() + "}"); + if (property.getKey().contains("env.")) { + // We don't want environment variables at the top of the completion proposals + item.setSortText("z${zzz" + property.getKey() + "}"); + } + return item; + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return toErrorCompletionItem(e); } - return item; - } catch (BadLocationException e) { - LOGGER.log(Level.SEVERE, e.getMessage(), e); - return toErrorCompletionItem(e); - } - }).collect(Collectors.toList()); + }).collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } private CompletionItem toErrorCompletionItem(Throwable e) { @@ -855,8 +925,10 @@ private CompletionItem toErrorCompletionItem(Throwable e) { return res; } - private void internalCollectRemoteGAVCompletion(ICompletionRequest request, boolean onlyPlugins, - Collection artifactInfosCollector, LinkedHashMap nonArtifactCollector, CancelChecker cancelChecker) { + private void internalCollectRemoteGAVCompletion(ICompletionRequest request, + boolean onlyPlugins, Collection artifactInfosCollector, + LinkedHashMap nonArtifactCollector, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); DOMElement node = request.getParentElement(); Dependency artifactToSearch = MavenParseUtils.parseArtifact(node); if (artifactToSearch == null) { @@ -866,16 +938,12 @@ private void internalCollectRemoteGAVCompletion(ICompletionRequest request, bool int startTagCloseOffset = node.getStartTagCloseOffset() >= 0 ? node.getStartTagCloseOffset() : 0; int endTagOpenOffset = node.getEndTagOpenOffset() >= 0 ? node.getEndTagOpenOffset() : request.getOffset(); Range range = XMLPositionUtility.createRange(startTagCloseOffset + 1, endTagOpenOffset, doc); - Set updateItems = Collections.synchronizedSet(new HashSet<>(1)); - final CompletionItem updatingItem = new CompletionItem(WAITING_LABEL); - updatingItem.setPreselect(false); - updatingItem.setInsertText(""); - updatingItem.setKind(CompletionItemKind.Event); - updateItems.add(updatingItem); + cancelChecker.checkCanceled(); plugin.getCentralSearcher().ifPresent(centralSearcher -> { cancelChecker.checkCanceled(); try { + cancelChecker.checkCanceled(); switch (node.getLocalName()) { case GROUP_ID_ELT: // TODO: just pass only plugins boolean, and make getGroupId's accept a boolean @@ -919,7 +987,6 @@ private void internalCollectRemoteGAVCompletion(ICompletionRequest request, bool cancelChecker.checkCanceled(); return; } - updateItems.remove(updatingItem); } catch (OngoingOperationException e) { // The remote searcher cannot return results right now, so // There is nothing to add to the collectors @@ -928,57 +995,72 @@ private void internalCollectRemoteGAVCompletion(ICompletionRequest request, bool // item like `Waiting for Maven Central Response...` in the // result } - updateItems.forEach(updateItem -> nonArtifactCollector.put(updateItem.getLabel(), updatingItem)); }); } @SuppressWarnings("deprecation") - private Collection collectSubModuleCompletion(ICompletionRequest request) { + private Collection collectSubModuleCompletion(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); DOMDocument doc = request.getXMLDocument(); File docFolder = new File(URI.create(doc.getTextDocument().getUri())).getParentFile(); String prefix = request.getNode().getNodeValue() != null ? request.getNode().getNodeValue() : ""; File prefixFile = new File(docFolder, prefix); List files = new ArrayList<>(); + cancelChecker.checkCanceled(); if (!prefix.isEmpty() && !prefix.endsWith("/")) { files.addAll(Arrays.asList( prefixFile.getParentFile().listFiles((dir, name) -> name.startsWith(prefixFile.getName())))); } + cancelChecker.checkCanceled(); if (prefixFile.isDirectory()) { files.addAll(Arrays.asList(prefixFile.listFiles(File::isDirectory))); } + cancelChecker.checkCanceled(); // make folder that have a pom show higher files.sort(Comparator.comparing((File file) -> new File(file, Maven.POMv4).exists()).reversed() .thenComparing(Function.identity())); + cancelChecker.checkCanceled(); if (prefix.isEmpty()) { files.add(docFolder.getParentFile()); } - return files.stream().map(file -> toFileCompletionItem(file, docFolder, request)).collect(Collectors.toList()); + cancelChecker.checkCanceled(); + var result = files.stream().map(file -> toFileCompletionItem(file, docFolder, request, cancelChecker)).collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } - private Collection collectMojoParametersDefaultCompletion(ICompletionRequest request, Set parameters) { + private Collection collectMojoParametersDefaultCompletion(ICompletionRequest request, + Set parameters, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); String prefix = request.getNode().getNodeValue() != null ? request.getNode().getNodeValue() : ""; List defaultValues = parameters.stream().filter(p -> (p.getDefaultValue() != null)) .map(p -> p.getDefaultValue()) .collect(Collectors.toList()); + cancelChecker.checkCanceled(); if (!prefix.isEmpty()) { defaultValues = defaultValues.stream().filter(v -> v.startsWith(prefix)) .collect(Collectors.toList()); } - return defaultValues.stream() + cancelChecker.checkCanceled(); + var result = defaultValues.stream() .sorted(String.CASE_INSENSITIVE_ORDER) .map(defaultValue -> toCompletionItem(defaultValue.toString(), null, request.getReplaceRange())) .collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } @SuppressWarnings("deprecation") - private Collection collectRelativePathCompletion(ICompletionRequest request) { + private Collection collectRelativePathCompletion(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); DOMDocument doc = request.getXMLDocument(); File docFile = new File(URI.create(doc.getTextDocument().getUri())); File docFolder = docFile.getParentFile(); String prefix = request.getNode().getNodeValue() != null ? request.getNode().getNodeValue() : ""; File prefixFile = new File(docFolder, prefix); List files = new ArrayList<>(); + cancelChecker.checkCanceled(); if (prefix.isEmpty()) { Arrays.stream(docFolder.getParentFile().listFiles()).filter(file -> file.getName().contains(PARENT_ELT)) .map(file -> new File(file, Maven.POMv4)).filter(File::isFile).forEach(files::add); @@ -995,11 +1077,14 @@ private Collection collectRelativePathCompletion(ICompletionRequ .listFiles(file -> file.getName().startsWith(thePrefixFile.getName())))); } } + cancelChecker.checkCanceled(); if (prefixFile.isDirectory()) { files.addAll(Arrays.asList(prefixFile.listFiles())); } - return files.stream().filter(file -> file.getName().equals(Maven.POMv4) || file.isDirectory()) + cancelChecker.checkCanceled(); + var result = files.stream().filter(file -> file.getName().equals(Maven.POMv4) || file.isDirectory()) .filter(file -> !(file.equals(docFolder) || file.equals(docFile))).flatMap(file -> { + cancelChecker.checkCanceled(); if (docFile.toPath().startsWith(file.toPath()) || file.getName().contains(PARENT_ELT)) { File pomFile = new File(file, Maven.POMv4); if (pomFile.exists()) { @@ -1020,16 +1105,20 @@ private Collection collectRelativePathCompletion(ICompletionRequ .thenComparing(file -> file.getParentFile().getParentFile().equals(docFolder.getParentFile())) // siblings // before... .reversed().thenComparing(Function.identity())// other folders and files - ).map(file -> toFileCompletionItem(file, docFolder, request)).collect(Collectors.toList()); + ).map(file -> toFileCompletionItem(file, docFolder, request, cancelChecker)).collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } - private Collection collectRelativeAnyPathCompletion(ICompletionRequest request) { + private Collection collectRelativeAnyPathCompletion(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); DOMDocument doc = request.getXMLDocument(); File docFile = new File(URI.create(doc.getTextDocument().getUri())); File docFolder = docFile.getParentFile(); String prefix = request.getNode().getNodeValue() != null ? request.getNode().getNodeValue() : ""; File prefixFile = new File(docFolder, prefix); List files = new ArrayList<>(); + cancelChecker.checkCanceled(); if (prefix.isEmpty()) { files.add(docFolder.getParentFile()); } else { @@ -1044,10 +1133,12 @@ private Collection collectRelativeAnyPathCompletion(ICompletionR .listFiles(file -> file.getName().startsWith(thePrefixFile.getName())))); } } + cancelChecker.checkCanceled(); if (prefixFile.isDirectory()) { files.addAll(Arrays.asList(prefixFile.listFiles())); } - return files.stream().filter(file -> file.isFile() || file.isDirectory()) + cancelChecker.checkCanceled(); + var result = files.stream().filter(file -> file.isFile() || file.isDirectory()) .sorted(Comparator.comparing(File::isFile) // files before folders .thenComparing( file -> (file.isFile() && docFile.toPath().startsWith(file.getParentFile().toPath())) @@ -1059,16 +1150,20 @@ private Collection collectRelativeAnyPathCompletion(ICompletionR .thenComparing(file -> file.getParentFile().getParentFile().equals(docFolder.getParentFile())) // siblings // before... .reversed().thenComparing(Function.identity())// other folders and files - ).map(file -> toFileCompletionItem(file, docFolder, request)).collect(Collectors.toList()); + ).map(file -> toFileCompletionItem(file, docFolder, request, cancelChecker)).collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } - private Collection collectRelativeDirectoryPathCompletion(ICompletionRequest request) { + private Collection collectRelativeDirectoryPathCompletion(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); DOMDocument doc = request.getXMLDocument(); File docFile = new File(URI.create(doc.getTextDocument().getUri())); File docFolder = docFile.getParentFile(); String prefix = request.getNode().getNodeValue() != null ? request.getNode().getNodeValue() : ""; File prefixFile = new File(docFolder, prefix); List files = new ArrayList<>(); + cancelChecker.checkCanceled(); if (prefix.isEmpty()) { files.add(docFolder.getParentFile()); } else { @@ -1083,10 +1178,12 @@ private Collection collectRelativeDirectoryPathCompletion(ICompl .listFiles(file -> file.getName().startsWith(thePrefixFile.getName())))); } } + cancelChecker.checkCanceled(); if (prefixFile.isDirectory()) { files.addAll(Arrays.asList(prefixFile.listFiles())); } - return files.stream().filter(file -> file.isDirectory()) + cancelChecker.checkCanceled(); + var result = files.stream().filter(file -> file.isDirectory()) .filter( file -> !file.equals(docFolder)) .sorted(Comparator.comparing(File::isDirectory) // only folders .thenComparing(file -> (file.isDirectory() && file.equals(docFolder.getParentFile()))) @@ -1094,7 +1191,9 @@ private Collection collectRelativeDirectoryPathCompletion(ICompl // "parent" before... .thenComparing(file -> file.getParentFile().getParentFile().equals(docFolder.getParentFile())) // siblings before... .reversed().thenComparing(Function.identity())// other folders and files - ).map(file -> toFileCompletionItem(file, docFolder, request)).collect(Collectors.toList()); + ).map(file -> toFileCompletionItem(file, docFolder, request, cancelChecker)).collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } private List collectRelativePropertiesFiles(File parent) { @@ -1108,13 +1207,15 @@ private List collectRelativePropertiesFiles(File parent) { return result; } - private Collection collectRelativeFilterPathCompletion(ICompletionRequest request) { + private Collection collectRelativeFilterPathCompletion(ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); DOMDocument doc = request.getXMLDocument(); File docFile = new File(URI.create(doc.getTextDocument().getUri())); File docFolder = docFile.getParentFile(); String prefix = request.getNode().getNodeValue() != null ? request.getNode().getNodeValue() : ""; File prefixFile = new File(docFolder, prefix); List files = new ArrayList<>(); + cancelChecker.checkCanceled(); if (!prefix.isEmpty()) { try { prefixFile = prefixFile.getCanonicalFile(); @@ -1128,23 +1229,29 @@ private Collection collectRelativeFilterPathCompletion(ICompleti && file.getName().endsWith(".properties"))))); } } + cancelChecker.checkCanceled(); if (prefixFile.isDirectory()) { files.addAll(collectRelativePropertiesFiles(prefixFile)); } - return files.stream() + cancelChecker.checkCanceled(); + var result = files.stream() .sorted(Comparator.comparing(File::isFile) // pom files before folders .thenComparing( file -> (file.isFile() && docFile.toPath().startsWith(file.getParentFile().toPath()))) .reversed().thenComparing(Function.identity())// other folders and files - ).map(file -> toFileCompletionItem(file, docFolder, request)).collect(Collectors.toList()); + ).map(file -> toFileCompletionItem(file, docFolder, request, cancelChecker)).collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } - private CompletionItem toFileCompletionItem(File file, File referenceFolder, ICompletionRequest request) { + private CompletionItem toFileCompletionItem(File file, File referenceFolder, ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); CompletionItem res = new CompletionItem(); Path path = referenceFolder.toPath().relativize(file.toPath()); StringBuilder builder = new StringBuilder(path.toString().length()); Path current = path; while (current != null) { + cancelChecker.checkCanceled(); if (!current.equals(path)) { // Only append "/" for parent directories builder.insert(0, '/'); @@ -1153,6 +1260,7 @@ private CompletionItem toFileCompletionItem(File file, File referenceFolder, ICo current = current.getParent(); } + cancelChecker.checkCanceled(); Range replaceRange = request.getReplaceRange(); /* @@ -1176,18 +1284,21 @@ private CompletionItem toFileCompletionItem(File file, File referenceFolder, ICo LOGGER.log(Level.SEVERE, e.getMessage(), e); } + cancelChecker.checkCanceled(); String pathString = builder.toString(); res.setLabel(pathString); res.setFilterText(pathString); res.setKind(file.isDirectory() ? CompletionItemKind.Folder : CompletionItemKind.File); res.setTextEdit(Either.forLeft(new TextEdit(replaceRange, pathString))); + cancelChecker.checkCanceled(); return res; } private Collection collectSimpleCompletionItems(Collection items, Function insertionTextExtractor, Function documentationExtractor, - ICompletionRequest request) { + ICompletionRequest request, CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); DOMElement node = request.getParentElement(); DOMDocument doc = request.getXMLDocument(); boolean needClosingTag = node.getEndTagOpenOffset() == DOMNode.NULL_VALUE; @@ -1195,26 +1306,31 @@ private Collection collectSimpleCompletionItems(Collection collectedItemLabels = Collections.synchronizedSet(new HashSet<>()); - return items.stream().map(o -> { - String label = insertionTextExtractor.apply(o); - CompletionItem item = new CompletionItem(); - item.setLabel(label); - String insertText = label + (needClosingTag ? "" : ""); - item.setKind(CompletionItemKind.Property); - item.setDocumentation(Either.forLeft(documentationExtractor.apply(o))); - item.setFilterText(insertText); - item.setTextEdit(Either.forLeft(new TextEdit(range, insertText))); - item.setInsertTextFormat(InsertTextFormat.PlainText); - return item; - }) - .filter(completionItem -> { - if (!collectedItemLabels.contains(completionItem.getLabel())) { - collectedItemLabels.add(completionItem.getLabel()); - return true; - } - return false; - }) - .collect(Collectors.toList()); + cancelChecker.checkCanceled(); + var result = items.stream().map(o -> { + cancelChecker.checkCanceled(); + String label = insertionTextExtractor.apply(o); + CompletionItem item = new CompletionItem(); + item.setLabel(label); + String insertText = label + (needClosingTag ? "" : ""); + item.setKind(CompletionItemKind.Property); + item.setDocumentation(Either.forLeft(documentationExtractor.apply(o))); + item.setFilterText(insertText); + item.setTextEdit(Either.forLeft(new TextEdit(range, insertText))); + item.setInsertTextFormat(InsertTextFormat.PlainText); + return item; + }) + .filter(completionItem -> { + cancelChecker.checkCanceled(); + if (!collectedItemLabels.contains(completionItem.getLabel())) { + collectedItemLabels.add(completionItem.getLabel()); + return true; + } + return false; + }) + .collect(Collectors.toList()); + cancelChecker.checkCanceled(); + return result; } /** @@ -1250,10 +1366,15 @@ public static Predicate distinctByKey(Function keyExtr return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } - private void internalCollectWorkspaceArtifacts(ICompletionRequest request, Collection artifactInfosCollector, - LinkedHashMap nonArtifactCollector, Optional groupId, Optional artifactId) { + private void internalCollectWorkspaceArtifacts(ICompletionRequest request, + Collection artifactInfosCollector, + LinkedHashMap nonArtifactCollector, + Optional groupId, Optional artifactId, + CancelChecker cancelChecker) { + cancelChecker.checkCanceled(); DOMElement parent = request.getParentElement(); if (parent == null || parent.getLocalName() == null) { + cancelChecker.checkCanceled(); return; } DOMElement grandParent = parent.getParentElement(); @@ -1261,6 +1382,7 @@ private void internalCollectWorkspaceArtifacts(ICompletionRequest request, Colle (!PARENT_ELT.equals(grandParent.getLocalName()) && !DEPENDENCY_ELT.equals(grandParent.getLocalName()) && !PLUGIN_ELT.equals(grandParent.getLocalName()))) { + cancelChecker.checkCanceled(); return; } @@ -1271,28 +1393,30 @@ private void internalCollectWorkspaceArtifacts(ICompletionRequest request, Colle int endTagOpenOffset = parent.getEndTagOpenOffset() >= 0 ? parent.getEndTagOpenOffset() : request.getOffset(); Range range = XMLPositionUtility.createRange(startTagCloseOffset + 1, endTagOpenOffset, doc); + cancelChecker.checkCanceled(); switch (parent.getLocalName()) { case ARTIFACT_ID_ELT: - plugin.getCurrentWorkspaceProjects().stream() // + plugin.getCurrentWorkspaceProjects(false).stream() // .filter(a -> groupIdFilter == null || groupIdFilter.equals(a.getGroupId())) .map(ArtifactWithDescription::new) // .forEach(artifactInfosCollector::add); break; case GROUP_ID_ELT: - plugin.getCurrentWorkspaceProjects().stream() // + plugin.getCurrentWorkspaceProjects(false).stream() // .filter(p -> artifactIdFilter == null || artifactIdFilter.equals(p.getArtifactId())) // .map(p -> toCompletionItem(p.getGroupId(), null, range)) // .filter(completionItem -> !nonArtifactCollector.containsKey(completionItem.getLabel())) .forEach(completionItem -> nonArtifactCollector.put(completionItem.getLabel(), completionItem)); break; case VERSION_ELT: - plugin.getCurrentWorkspaceProjects().stream() // + plugin.getCurrentWorkspaceProjects(false).stream() // .filter(p -> artifactIdFilter == null || artifactIdFilter.equals(p.getArtifactId())) // .map(p -> toCompletionItem(p.getVersion(), null, range)) // .filter(completionItem -> !nonArtifactCollector.containsKey(completionItem.getLabel())) .forEach(completionItem -> nonArtifactCollector.put(completionItem.getLabel(), completionItem)); break; } + cancelChecker.checkCanceled(); } private static boolean isInsertTextModeAdjustIndentationSupport(ICompletionRequest request) { diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/diagnostics/MavenDiagnosticParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/diagnostics/MavenDiagnosticParticipant.java index 2d60183e..532d53b4 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/diagnostics/MavenDiagnosticParticipant.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/diagnostics/MavenDiagnosticParticipant.java @@ -20,7 +20,11 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -46,6 +50,7 @@ import org.eclipse.lsp4j.jsonrpc.CancelChecker; public class MavenDiagnosticParticipant implements IDiagnosticsParticipant { + private static final Logger LOGGER = Logger.getLogger(MavenDiagnosticParticipant.class.getName()); private final MavenLemminxExtension plugin; @@ -61,31 +66,44 @@ public void doDiagnostics(DOMDocument xmlDocument, List diagnostics, } try { - LoadedMavenProject loadedMavenProject = plugin.getProjectCache().getLoadedMavenProject(xmlDocument); + CompletableFuture project = plugin.getProjectCache().getLoadedMavenProject(xmlDocument); + if (MavenLemminxExtension.isUnitTestMode()) { + try { + project.get(); + } catch (InterruptedException | ExecutionException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + } + if (!project.isDone()) { + // The pom.xml takes some times to load it, to avoid blocking the XML syntax validation, XML validation based on XSD + // we retrigger the validation when the pom.xml is loaded. + project.thenAccept( unused -> plugin.getValidationService() + .validate(xmlDocument)); + return; + } + + LoadedMavenProject loadedMavenProject = project.getNow(null); Collection problems = loadedMavenProject != null ? loadedMavenProject.getProblems() : null; if (problems != null) { - problems - .stream() - .map(problem -> toDiagnostic(problem, xmlDocument)) + problems.stream().map(problem -> toDiagnostic(problem, xmlDocument)) .forEach(diagnostics::add); } - DependencyResolutionResult dependencyResolutionResult = loadedMavenProject != null - ? loadedMavenProject.getDependencyResolutionResult() - : null; + DependencyResolutionResult dependencyResolutionResult = + loadedMavenProject != null ? + loadedMavenProject.getDependencyResolutionResult() : null; cancelChecker.checkCanceled(); DOMElement documentElement = xmlDocument.getDocumentElement(); if (documentElement == null) { return; } - Map>>> tagDiagnostics = configureDiagnosticFunctions( - cancelChecker); + Map>>> tagDiagnostics = + configureDiagnosticFunctions(cancelChecker); // Validate project element cancelChecker.checkCanceled(); if (PROJECT_ELT.equals(documentElement.getNodeName())) { - ProjectValidator projectValidator = new ProjectValidator(plugin, dependencyResolutionResult, - cancelChecker); + ProjectValidator projectValidator = new ProjectValidator(plugin, dependencyResolutionResult, cancelChecker); projectValidator.validateProject(new DiagnosticRequest(documentElement, xmlDocument)) .ifPresent(diagnosticList -> { cancelChecker.checkCanceled(); @@ -111,7 +129,6 @@ public void doDiagnostics(DOMDocument xmlDocument, List diagnostics, diagnosticList.stream().filter(diagnostic -> !diagnostics.contains(diagnostic)) .collect(Collectors.toList())); }); - ; } cancelChecker.checkCanceled(); if (node.hasChildNodes()) { diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java index 2aaf3ba6..9c396d75 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java @@ -487,7 +487,7 @@ private Hover collectPluginConfiguration(IHoverRequest request, CancelChecker ca Set parameters; cancelChecker.checkCanceled(); try { - parameters = MavenPluginUtils.collectPluginConfigurationMojoParameters(request, plugin); + parameters = MavenPluginUtils.collectPluginConfigurationMojoParameters(request, plugin, cancelChecker); } catch (PluginResolutionException | PluginDescriptorParsingException | InvalidPluginDescriptorException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); return null; diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipant.java index 0f496489..f6abfabf 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipant.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipant.java @@ -148,7 +148,7 @@ public void doRename(IRenameRequest request, IRenameResponse renameResponse, Can cancelChecker.checkCanceled(); LinkedHashSet projects = new LinkedHashSet<>(); projects.add(thisProject); - plugin.getCurrentWorkspaceProjects().stream().forEach(child -> + plugin.getCurrentWorkspaceProjects(true).stream().forEach(child -> projects.addAll(findParentsOfChildProject(thisProject, child))); URI thisProjectUri = ParticipantUtils.normalizedUri(document.getDocumentURI()); @@ -178,7 +178,6 @@ public void doRename(IRenameRequest request, IRenameResponse renameResponse, Can // - Maven is initializing // - or parse of maven model with DOM document is out of dated // -> catch the error to avoid breaking XML rename from LemMinX - } } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/DOMModelSource.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/DOMModelSource.java index 67ed4ccb..6dbe96a3 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/DOMModelSource.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/DOMModelSource.java @@ -8,14 +8,14 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.maven.utils; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.net.URI; +import java.util.Objects; import org.apache.maven.model.building.FileModelSource; import org.apache.maven.model.building.ModelSource; import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.utils.FilesUtils; /** * A Maven {@link ModelSource} implementation based on LemMinx @@ -33,7 +33,7 @@ public class DOMModelSource extends FileModelSource { private final DOMDocument document; public DOMModelSource(DOMDocument document) { - super(new File(URI.create(document.getDocumentURI()).normalize())); + super(FilesUtils.toFile(document.getDocumentURI())); this.document = document; } @@ -41,4 +41,9 @@ public DOMModelSource(DOMDocument document) { public InputStream getInputStream() throws IOException { return new DOMInputStream(document); } + + @Override + public int hashCode() { + return Objects.hash(getFile(), document.getTextDocument().getVersion()); + } } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MavenPluginUtils.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MavenPluginUtils.java index 1dcd49d7..46e2e781 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MavenPluginUtils.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MavenPluginUtils.java @@ -109,19 +109,22 @@ public static Set collectPluginConfigurationParameters(IPositionReque } public static Set collectPluginConfigurationMojoParameters(IPositionRequest request, - MavenLemminxExtension plugin) + MavenLemminxExtension plugin, CancelChecker cancelChecker) throws PluginResolutionException, PluginDescriptorParsingException, InvalidPluginDescriptorException { + cancelChecker.checkCanceled(); PluginDescriptor pluginDescriptor = null; try { pluginDescriptor = MavenPluginUtils.getContainingPluginDescriptor(request.getNode(), plugin); } catch (PluginResolutionException | PluginDescriptorParsingException | InvalidPluginDescriptorException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } + cancelChecker.checkCanceled(); if (pluginDescriptor == null) { return Collections.emptySet(); } List mojosToConsiderList = pluginDescriptor.getMojos(); DOMNode executionElementDomNode = DOMUtils.findClosestParentNode(request.getNode(), EXECUTION_ELT); + cancelChecker.checkCanceled(); if (executionElementDomNode != null) { Set interestingMojos = executionElementDomNode.getChildren().stream() .filter(node -> GOALS_ELT.equals(node.getLocalName())).flatMap(node -> node.getChildren().stream()) @@ -131,14 +134,18 @@ public static Set collectPluginConfigurationMojoParameters(IPosit .collect(Collectors.toList()); } MavenProject project = plugin.getProjectCache().getLastSuccessfulMavenProject(request.getXMLDocument()); + cancelChecker.checkCanceled(); if (project == null) { return Collections.emptySet(); } plugin.getMavenSession().setProjects(Collections.singletonList(project)); final var finalPluginDescriptor = pluginDescriptor; - return mojosToConsiderList.stream().flatMap(mojo -> PlexusConfigHelper + cancelChecker.checkCanceled(); + var result = mojosToConsiderList.stream().flatMap(mojo -> PlexusConfigHelper .loadMojoParameters(finalPluginDescriptor, mojo, plugin.getMavenSession(), plugin.getBuildPluginManager()) .stream()).collect(Collectors.toSet()); + cancelChecker.checkCanceled(); + return result; } public static RemoteRepository toRemoteRepo(Repository modelRepo) { diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/URIUtils.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/URIUtils.java new file mode 100644 index 00000000..3021b544 --- /dev/null +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/URIUtils.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * 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 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lemminx.extensions.maven.utils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.lemminx.dom.DOMDocument; + +public class URIUtils { + private static final Logger LOGGER = Logger.getLogger(URIUtils.class.getName()); + + /** + * Returns an URI String identifying a specified document + * + * @param document A Maven Project document + * @return URI String identificator + */ + public static String toURIString(DOMDocument document) { + return document.getDocumentURI(); + } + + /** + * Returns an URI String identifying a specified POM file + * + * @param file A POM file + * @return URI String identificator + */ + public static String toURIString(File file) { + return toUri(file).toASCIIString(); + } + + /** + * Returns an URI String key for a specified document to be used to store + * a Maven Project document information in a map + * + * @param document A Maven Project document + * @return URI String key + */ + public static String toURIKey(DOMDocument document) { + return toURIKey(toURIString(document)); + } + + /** + * Returns an URI String key for a specified URI String identificator to be + * used to store a Maven Project document information in a map + * + * @param uriString URI String identificator + * @return URI String key + */ + public static String toURIKey(String uriString) { + String normalizedURI = URI.create(uriString).normalize().toASCIIString() ; + return URIUtils.encodeFileURI(normalizedURI, StandardCharsets.UTF_8) + .toUpperCase(); + } + + /** + * Returns an URI String key for a specified POM file to be used to store + * a Maven Project document information in a map + * + * @param file POM file + * @return URI String key + */ + public static String toURIKey(File file) { + return toURIString(file).toUpperCase(); + } + + // Copied from https://github.com/eclipse/lsp4e/blob/master/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java + private static URI toUri(File file) { + // URI scheme specified by language server protocol and LSP + try { + return new URI("file", "", file.getAbsoluteFile().toURI().getPath(), null); //$NON-NLS-1$ //$NON-NLS-2$ + } catch (URISyntaxException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return file.getAbsoluteFile().toURI(); + } + } + + // Copied from https://github.com/eclipse/lsp4mp/blob/master/microprofile.ls/org.eclipse.lsp4mp.ls/src/main/java/org/eclipse/lsp4mp/utils/URIUtils.java + private static String encodeFileURI(String source, Charset charset) { + String fileScheme = ""; + int index = -1; + if (source.startsWith("file://")) { + index = 6; + if (source.charAt(7) == '/') { + index = 7; + } + } + if (index != -1) { + fileScheme = source.substring(0, index + 1); + source = source.substring(index + 1, source.length()); + } + + byte[] bytes = source.getBytes(charset); + boolean original = true; + for (byte b : bytes) { + if (!isAllowed(b)) { + original = false; + break; + } + } + if (original) { + return source; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.length); + for (byte b : bytes) { + if (isAllowed(b)) { + baos.write(b); + } else { + baos.write('%'); + char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); + char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); + baos.write(hex1); + baos.write(hex2); + } + } + return fileScheme + copyToString(baos, charset); + } + + /** + * Copy the contents of the given {@link ByteArrayOutputStream} into a + * {@link String}. + *

+ * This is a more effective equivalent of + * {@code new String(baos.toByteArray(), charset)}. + * + * @param baos the {@code ByteArrayOutputStream} to be copied into a String + * @param charset the {@link Charset} to use to decode the bytes + * @return the String that has been copied to (possibly empty) + */ + private static String copyToString(ByteArrayOutputStream baos, Charset charset) { + try { + // Can be replaced with toString(Charset) call in Java 10+ + return baos.toString(charset.name()); + } catch (UnsupportedEncodingException ex) { + // Should never happen + throw new IllegalArgumentException("Invalid charset name: " + charset, ex); + } + } + + private static boolean isAllowed(int c) { + return isUnreserved(c) || '/' == c; + } + + /** + * Indicates whether the given character is in the {@code unreserved} set. + * + * @see RFC 3986, appendix A + */ + private static boolean isUnreserved(int c) { + return (isAlpha(c) || isDigit(c) || '-' == c || '.' == c || '_' == c || '~' == c); + } + + /** + * Indicates whether the given character is in the {@code ALPHA} set. + * + * @see RFC 3986, appendix A + */ + private static boolean isAlpha(int c) { + return (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'); + } + + /** + * Indicates whether the given character is in the {@code DIGIT} set. + * + * @see RFC 3986, appendix A + */ + private static boolean isDigit(int c) { + return (c >= '0' && c <= '9'); + } +} diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/MavenLanguageService.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/MavenLanguageService.java index 563f3d37..4995794f 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/MavenLanguageService.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/MavenLanguageService.java @@ -8,6 +8,11 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.maven; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.services.IXMLDocumentProvider; import org.eclipse.lemminx.services.XMLLanguageService; /** @@ -18,8 +23,24 @@ */ public class MavenLanguageService extends XMLLanguageService{ + private Map documents = new HashMap<>(); + public MavenLanguageService() { MavenLemminxExtension.setUnitTestMode(true); + setDocumentProvider(new IXMLDocumentProvider() { + + @Override + public DOMDocument getDocument(String uri) { + return documents.get(uri); + } + }); + } + + public void didOpen(DOMDocument document) { + // We need to store the dom document instance that test created because + // the dpcument content can changes in the test and when getDocument(String uri) + // will be called it need to use this instance otherwise it will get the + // instance loaded from the file which have not changed + documents.put(document.getDocumentURI(), document); } - } diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/MavenProjectCacheTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/MavenProjectCacheTest.java index 8e0afbcb..cda56f5a 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/MavenProjectCacheTest.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/MavenProjectCacheTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Red Hat Inc. and others. + * Copyright (c) 2019-2023 Red Hat Inc. and others. * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ @@ -29,25 +29,37 @@ import org.eclipse.lemminx.commons.TextDocument; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationSettings; -import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lemminx.services.extensions.IWorkspaceServiceParticipant; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(NoMavenCentralExtension.class) public class MavenProjectCacheTest { - + private static MavenLanguageService languageService; + + @BeforeEach + public void setUp() { + MavenLemminxExtension.setUnitTestMode(true); + // Some tests require like a "cold: start with no any information cached + // So the language service is to be created at @BeforeEach + languageService = new MavenLanguageService(); + } + @Test public void testSimpleProjectIsParsed() throws Exception { + MavenLemminxExtension plugin = new MavenLemminxExtension(); + plugin.start(null,languageService); + URI uri = getClass().getResource("/pom-with-properties.xml").toURI(); String content = Files.readString(new File(uri).toPath(), StandardCharsets.UTF_8); DOMDocument doc = new DOMDocument(new TextDocument(content, uri.toString()), null); - MavenLemminxExtension plugin = new MavenLemminxExtension(); - MavenLemminxExtension.setUnitTestMode(true); + languageService.didOpen(doc); + MavenProjectCache cache = plugin.getProjectCache(); MavenProject project = cache.getLastSuccessfulMavenProject(doc); assertNotNull(project); @@ -55,12 +67,15 @@ public void testSimpleProjectIsParsed() throws Exception { @Test public void testOnBuildError_ResolveProjectFromDocumentBytes() throws Exception { + MavenLemminxExtension plugin = new MavenLemminxExtension(); + plugin.start(null,languageService); + URI uri = getClass().getResource("/pom-with-module-error.xml").toURI(); File pomFile = new File(uri); String content = Files.readString(pomFile.toPath(), StandardCharsets.UTF_8); DOMDocument doc = new DOMDocument(new TextDocument(content, uri.toString()), null); - MavenLemminxExtension plugin = new MavenLemminxExtension(); - MavenLemminxExtension.setUnitTestMode(true); + languageService.didOpen(doc); + MavenProjectCache cache = plugin.getProjectCache(); MavenProject project = cache.getLastSuccessfulMavenProject(doc); assertNotNull(project); @@ -70,9 +85,12 @@ public void testOnBuildError_ResolveProjectFromDocumentBytes() throws Exception public void testParentChangeReflectedToChild() throws IOException, InterruptedException, ExecutionException, URISyntaxException, Exception { MavenLemminxExtension plugin = new MavenLemminxExtension(); - MavenLemminxExtension.setUnitTestMode(true); + plugin.start(null,languageService); + MavenProjectCache cache = plugin.getProjectCache(); - DOMDocument doc = getDocument("/pom-with-properties-in-parent.xml"); + DOMDocument doc = createDOMDocument("/pom-with-properties-in-parent.xml", languageService); + languageService.didOpen(doc); + MavenProject project = cache.getLastSuccessfulMavenProject(doc); assertTrue(project.getProperties().containsKey("myProperty"), project.getProperties().toString()); URI parentUri = getClass().getResource("/pom-with-properties.xml").toURI(); @@ -92,7 +110,6 @@ public void testParentChangeReflectedToChild() @Test public void testAddFolders_didChangeWorkspaceFolders() throws Exception { - XMLLanguageService languageService = new MavenLanguageService(); IWorkspaceServiceParticipant workspaceService = languageService.getWorkspaceServiceParticipants().stream().filter(MavenWorkspaceService.class::isInstance).findAny().get(); assertNotNull(workspaceService); @@ -111,13 +128,14 @@ public void testAddFolders_didChangeWorkspaceFolders() throws Exception { // error messages should appear. // DOMDocument doc = createDOMDocument("/modules/dependent/module-c-pom.xml", languageService); + languageService.didOpen(doc); + List diagnostics = languageService.doDiagnostics(doc, new XMLValidationSettings(), Map.of(), () -> {}); assertFalse(diagnostics.stream().anyMatch(diag -> (diag.getMessage().contains("ModuleA")))); } // @Test public void testRemoveFolders_didChangeWorkspaceFolders() throws Exception { - XMLLanguageService languageService = new MavenLanguageService(); IWorkspaceServiceParticipant workspaceService = languageService.getWorkspaceServiceParticipants().stream().filter(MavenWorkspaceService.class::isInstance).findAny().get(); assertNotNull(workspaceService); @@ -143,6 +161,8 @@ public void testRemoveFolders_didChangeWorkspaceFolders() throws Exception { // error message should appear. // DOMDocument doc = createDOMDocument("/modules/dependent/module-c-pom.xml", languageService); + languageService.didOpen(doc); + List diagnostics = languageService.doDiagnostics(doc, new XMLValidationSettings(), Map.of(), () -> {}); assertTrue(diagnostics.stream().anyMatch(diag -> (diag.getMessage().contains("ModuleA")))); } @@ -151,9 +171,9 @@ public void testRemoveFolders_didChangeWorkspaceFolders() throws Exception { public void testNormilizePathsAreUsedInCache() throws IOException, InterruptedException, ExecutionException, URISyntaxException, Exception { MavenLemminxExtension plugin = new MavenLemminxExtension(); - MavenLemminxExtension.setUnitTestMode(true); + plugin.start(null,languageService); + MavenProjectCache cache = plugin.getProjectCache(); - int initialProjectsSize = cache.getProjects().size(); // Use URLretative to a child to access a parent pom.xml @@ -221,12 +241,4 @@ private DOMDocument getDocumentNotNormilized(String resource) throws URISyntaxEx DOMDocument doc = new DOMDocument(new TextDocument(content, pomFile.toURI().toString()), null); return doc; } - - private DOMDocument getDocument(String resource) throws URISyntaxException, IOException { - URI uri = getClass().getResource(resource).toURI(); - File pomFile = new File(uri); - String content = Files.readString(pomFile.toPath(), StandardCharsets.UTF_8); - DOMDocument doc = new DOMDocument(new TextDocument(content, uri.toString()), null); - return doc; - } } diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/SimpleModelTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/SimpleModelTest.java index ca6550f0..7490decb 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/SimpleModelTest.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/SimpleModelTest.java @@ -40,7 +40,6 @@ import org.eclipse.lemminx.extensions.maven.NoMavenCentralExtension; import org.eclipse.lemminx.extensions.maven.utils.DOMUtils; import org.eclipse.lemminx.extensions.maven.utils.MavenLemminxTestsUtils; -import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lemminx.services.extensions.IWorkspaceServiceParticipant; import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lemminx.utils.XMLPositionUtility; @@ -68,7 +67,7 @@ @ExtendWith(NoMavenCentralExtension.class) public class SimpleModelTest { - private XMLLanguageService languageService; + private MavenLanguageService languageService; @BeforeEach public void setUp() throws IOException { @@ -85,28 +84,35 @@ public void tearDown() throws InterruptedException, ExecutionException { @Test @Timeout(10000) public void testScopeCompletion() throws IOException, InterruptedException, ExecutionException, URISyntaxException { - CompletionList completion = languageService.doComplete(createDOMDocument("/pom-with-module-error.xml", languageService), + DOMDocument document = createDOMDocument("/pom-with-module-error.xml", languageService); + languageService.didOpen(document); + + CompletionList completion = languageService.doComplete(document, new Position(12, 10), new SharedSettings()); assertTrue(completion.getItems().stream().map(CompletionItem::getLabel).anyMatch("runtime"::equals)); } - @Test @Timeout(10000) public void testPropertyCompletion() throws IOException, InterruptedException, ExecutionException, URISyntaxException { - CompletionList completion = languageService.doComplete(createDOMDocument("/pom-with-properties.xml", languageService), + DOMDocument document = createDOMDocument("/pom-with-properties.xml", languageService); + languageService.didOpen(document); + + CompletionList completion = languageService.doComplete(document, new Position(11, 15), new SharedSettings()); assertTrue(completion.getItems().stream().map(CompletionItem::getLabel).anyMatch(label -> label.contains("myProperty"))); assertTrue(completion.getItems().stream().map(CompletionItem::getLabel).anyMatch(label -> label.contains("project.build.directory"))); } - @Test @Timeout(10000) public void testParentPropertyCompletion() throws IOException, InterruptedException, ExecutionException, URISyntaxException { - assertTrue(languageService.doComplete(createDOMDocument("/pom-with-properties-in-parent.xml", languageService), new Position(15, 20), new SharedSettings()) + DOMDocument document = createDOMDocument("/pom-with-properties-in-parent.xml", languageService); + languageService.didOpen(document); + + assertTrue(languageService.doComplete(document, new Position(15, 20), new SharedSettings()) .getItems().stream().map(CompletionItem::getLabel).anyMatch(label -> label.contains("myProperty"))); } @@ -115,10 +121,17 @@ public void testParentPropertyCompletion() public void testLocalParentGAVCompletion() throws IOException, InterruptedException, ExecutionException, URISyntaxException, TimeoutException { // * if relativePath is set and resolve to a pom or a folder containing a pom, GAV must be available for completion - assertTrue(languageService.doComplete(createDOMDocument("/hierarchy/child/grandchild/pom.xml", languageService), + DOMDocument document = createDOMDocument("/hierarchy/child/grandchild/pom.xml", languageService); + languageService.didOpen(document); + + assertTrue(languageService.doComplete(document, new Position(4, 2), new SharedSettings()).getItems().stream().map(CompletionItem::getLabel).anyMatch(label -> label.startsWith("test-parent"))); + // * if relativePath is not set and parent contains a pom, complete GAV from parent - assertTrue(languageService.doComplete(createDOMDocument("/hierarchy/child/pom.xml", languageService), + document = createDOMDocument("/hierarchy/child/pom.xml", languageService); + languageService.didOpen(document); + + assertTrue(languageService.doComplete(document, new Position(4, 2), new SharedSettings()).getItems().stream().map(CompletionItem::getLabel).anyMatch(label -> label.startsWith("test-parent"))); // TODO: // * if relativePath is not set, complete with local repo artifacts with "pom" packaging @@ -130,6 +143,8 @@ public void testDoNotReportNonParseablePomError() throws IOException, InterruptedException, ExecutionException, URISyntaxException { TextDocument textDocument = new TextDocument(" < ", "file:///pom.xml"); DOMDocument document = DOMParser.getInstance().parse(textDocument, languageService.getResolverExtensionManager()); + languageService.didOpen(document); + List diagnostics = languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), () -> {}); assertFalse(diagnostics.stream().anyMatch(diag -> diag.getMessage().contains("Non-parseable POM"))); } @@ -138,13 +153,19 @@ public void testDoNotReportNonParseablePomError() public void testMissingArtifactIdError() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-without-artifactId.xml", languageService); - assertTrue(languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), () -> {}).stream().map(Diagnostic::getMessage) + languageService.didOpen(document); + + Listdiagnostics = languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), () -> {}); + System.out.println(diagnostics); + assertTrue(diagnostics.stream().map(Diagnostic::getMessage) .anyMatch(message -> message.contains("artifactId"))); // simulate an edit TextDocument textDocument = document.getTextDocument(); textDocument.setText(textDocument.getText().replace("", "a")); textDocument.setVersion(textDocument.getVersion() + 1); document = DOMParser.getInstance().parse(textDocument, languageService.getResolverExtensionManager()); + languageService.didOpen(document); + assertEquals(Collections.emptyList(), languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), () -> {})); } @@ -152,6 +173,8 @@ public void testMissingArtifactIdError() public void testSystemPathDiagnosticBug() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-environment-variable-property.xml", languageService); + languageService.didOpen(document); + List diagnostics = languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), () -> {}); assertFalse(diagnostics.stream().anyMatch(diag -> diag.getMessage().contains("${env"))); } @@ -159,7 +182,10 @@ public void testSystemPathDiagnosticBug() @Test public void testEnvironmentVariablePropertyHover() throws IOException, InterruptedException, ExecutionException, URISyntaxException { - String hoverContents = languageService.doHover(createDOMDocument("/pom-environment-variable-property.xml", languageService), + DOMDocument document = createDOMDocument("/pom-environment-variable-property.xml", languageService); + languageService.didOpen(document); + + String hoverContents = languageService.doHover(document, new Position(16, 18), new SharedSettings()).getContents().getRight().getValue(); // We can't test the value of an environment variable as it is platform-dependent assertNotNull(hoverContents); @@ -169,6 +195,8 @@ public void testEnvironmentVariablePropertyHover() public void testCompletionEnvironmentVariableProperty() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-environment-variable-property.xml", languageService); + languageService.didOpen(document); + List completions = languageService.doComplete(document, new Position(16, 49), new SharedSettings()).getItems(); assertTrue(completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) .anyMatch("${env.PATH}"::equals)); @@ -178,6 +206,8 @@ public void testCompletionEnvironmentVariableProperty() @Timeout(15000) public void testCompleteScope() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-scope.xml", languageService); + languageService.didOpen(document); + assertTrue(languageService.doComplete(document, new Position(0, 7), new SharedSettings()).getItems().stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) .anyMatch("compile"::equals)); assertTrue(languageService.doComplete(document, new Position(1, 7), new SharedSettings()).getItems().stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) @@ -188,6 +218,8 @@ public void testCompleteScope() throws IOException, InterruptedException, Execut @Timeout(15000) public void testCompletePhase() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-phase.xml", languageService); + languageService.didOpen(document); + assertTrue(languageService.doComplete(document, new Position(0, 7), new SharedSettings()).getItems().stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) .anyMatch("generate-resources"::equals)); assertTrue(languageService.doComplete(document, new Position(1, 7), new SharedSettings()).getItems().stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) @@ -197,6 +229,8 @@ public void testCompletePhase() throws IOException, InterruptedException, Execut @Test public void testPropertyHover() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-with-properties.xml", languageService); + languageService.didOpen(document); + Hover hover = languageService.doHover(document, new Position(15, 20), new SharedSettings()); assertTrue((((MarkupContent) hover.getContents().getRight()).getValue().contains("$"))); @@ -210,6 +244,8 @@ public void testPropertyHover() throws IOException, InterruptedException, Execut @Test public void testPropertyDefinitionSameDocument() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-with-properties-for-definition.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(14, 22); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); @@ -225,6 +261,8 @@ public void testPropertyDefinitionSameDocument() throws IOException, Interrupted @Test public void testMultiplePropertyHover() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-with-multiple-properties.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(16, 12); Hover hover = languageService.doHover(document,pos, new SharedSettings()); Range firstHoverRange = hover.getRange(); @@ -241,6 +279,8 @@ public void testMultiplePropertyHover() throws IOException, InterruptedException public void testMultiplePropertyDefinitionRangeSameTag() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-with-multiple-properties.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(16, 12); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); @@ -267,6 +307,8 @@ public void testMultiplePropertyDefinitionRangeSameTag() @Test public void testPropertyDefinitionSameDocumentBug() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-definition-wrong-tag-bug.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(22, 40); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); @@ -284,12 +326,16 @@ public void testPropertyDefinitionSameDocumentBug() throws IOException, Interrup @Test public void testPropertyDefinitionParentDocument() throws IOException, InterruptedException, ExecutionException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-with-properties-in-parent-for-definition.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(23, 16); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); //Verify the LocationLink points to the right file and node DOMDocument targetDocument = createDOMDocument("/pom-with-properties-for-definition.xml", languageService); + languageService.didOpen(document); + DOMNode propertyNode = DOMUtils.findNodesByLocalName(targetDocument, "myProperty").stream().filter(node -> node.getParentElement().getLocalName().equals("properties")).collect(Collectors.toList()).get(0);; Range expectedTargetRange = XMLPositionUtility.createRange(propertyNode); assertTrue(definitionLinks.stream().anyMatch(link -> link.getTargetUri().equals(targetDocument.getDocumentURI()))); @@ -300,43 +346,43 @@ public void testPropertyDefinitionParentDocument() throws IOException, Interrupt @Test public void testModuleDefinition() throws IOException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-module-definition.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(11, 5); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); DOMDocument targetDocument = createDOMDocument("/multi-module/pom.xml", languageService); + languageService.didOpen(document); + assertTrue(definitionLinks.stream().anyMatch(link -> link.getTargetUri().equals(targetDocument.getDocumentURI()))); } @Test public void testModules() throws IOException, URISyntaxException { + DOMDocument documentA = createDOMDocument("/modules/module-a-pom.xml", languageService); + languageService.didOpen(documentA); + List diagnosticsA = languageService.doDiagnostics( - createDOMDocument("/modules/module-a-pom.xml", languageService), new XMLValidationSettings(), Map.of(), - () -> { - }); + documentA, new XMLValidationSettings(), Map.of(), () -> {}); assertFalse(diagnosticsA.stream() .anyMatch(diag -> (diag.getMessage().contains("ModuleA") || diag.getMessage().contains("ModuleB")))); - DOMDocument document = createDOMDocument("/modules/dependent/module-b-pom.xml", languageService); - List diagnosticsB = languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), - () -> { - }); + DOMDocument documentB = createDOMDocument("/modules/dependent/module-b-pom.xml", languageService); + languageService.didOpen(documentB); + + List diagnosticsB = languageService.doDiagnostics( + documentB, new XMLValidationSettings(), Map.of(), () -> {}); // As there is not workspace folders initialized, there is the error // "Could not find artifact org.test.modules:ModuleA:jar:0.0.1-SNAPSHOT assertArrayEquals(new Diagnostic[] { d(9, 4, 13, 17, null, "", "xml", DiagnosticSeverity.Error), // - }, diagnosticsB - .stream() - .filter(d -> { - d.setMessage(""); - return true; - }) - .toList() - .toArray(new Diagnostic[diagnosticsB.size()])); + }, diagnosticsB.stream().filter(d -> {d.setMessage(""); return true;}) + .toList().toArray(new Diagnostic[diagnosticsB.size()])); } @Test - public void testModulesCompletionInDependency() throws IOException, URISyntaxException { + public void testModulesCompletionInDependency() throws IOException, URISyntaxException, InterruptedException { // We need the WORKSPACE projects to be placed to MavenProjectCache IWorkspaceServiceParticipant workspaceService = languageService.getWorkspaceServiceParticipants().stream().filter(MavenWorkspaceService.class::isInstance).findAny().get(); assertNotNull(workspaceService); @@ -351,36 +397,65 @@ public void testModulesCompletionInDependency() throws IOException, URISyntaxExc Arrays.asList(new WorkspaceFolder[] {wsFolder}), Arrays.asList(new WorkspaceFolder[0])))); + DOMDocument documentA = createDOMDocument("/modules/module-a-pom.xml", languageService); + languageService.didOpen(documentA); + List diagnosticsA = languageService.doDiagnostics( - createDOMDocument("/modules/module-a-pom.xml", languageService), - new XMLValidationSettings(), Map.of(), () -> {}); + documentA, new XMLValidationSettings(), Map.of(), () -> {}); assertFalse(diagnosticsA.stream().anyMatch(diag -> (diag.getMessage().contains("ModuleA") || diag.getMessage().contains("ModuleB")))); - DOMDocument document = createDOMDocument("/modules/dependent/module-b-pom.xml", languageService); + DOMDocument documentB = createDOMDocument("/modules/dependent/module-b-pom.xml", languageService); + languageService.didOpen(documentB); + List diagnosticsB = languageService.doDiagnostics( - document, - new XMLValidationSettings(), Map.of(), () -> {}); + documentB, new XMLValidationSettings(), Map.of(), () -> {}); assertFalse(diagnosticsB.stream().anyMatch(diag -> (diag.getMessage().contains("ModuleA") || diag.getMessage().contains("ModuleB")))); + // The items collected from Workspace as well as from Maven Search API cannot be + // immediately obtained due to the "lazy: loading, so, we need to wait until all + // the required data received and ready for use + // in // for group ID - List completions = languageService.doComplete(document, new Position(10, 15), new SharedSettings()).getItems(); - assertTrue(completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) - .anyMatch("org.test.modules"::equals)); - + List completions = null; + int triesLeft = 15; // given a 1-second `sleep` between the retries this gives >15 seconds overall timeout + boolean conditionMet = false; + do { + completions = languageService.doComplete( + documentB, new Position(10, 15), new SharedSettings()) + .getItems(); + Thread.sleep(1000); + conditionMet = completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) + .anyMatch("org.test.modules"::equals); + } while (!conditionMet && triesLeft-- > 0); + // for artifact ID: - completions = languageService.doComplete(document, new Position(11, 18), new SharedSettings()).getItems(); - assertTrue(completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) - .anyMatch("ModuleA"::equals)); + triesLeft = 15; // given a 1-second `sleep` between the retries this gives >15 seconds overall timeout + conditionMet = false; + do { + completions = languageService.doComplete( + documentB, new Position(11, 18), new SharedSettings()) + .getItems(); + Thread.sleep(1000); + conditionMet = completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) + .anyMatch("ModuleA"::equals); + } while (!conditionMet && triesLeft-- > 0); // for versions - completions = languageService.doComplete(document, new Position(12, 15), new SharedSettings()).getItems(); - assertTrue(completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) - .anyMatch("0.0.1-SNAPSHOT"::equals)); + triesLeft = 15; // given a 1-second `sleep` between the retries this gives >15 seconds overall timeout + conditionMet = false; + do { + completions = languageService.doComplete( + documentB, new Position(12, 15), new SharedSettings()) + .getItems(); + Thread.sleep(1000); + conditionMet = completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) + .anyMatch("0.0.1-SNAPSHOT"::equals); + } while (!conditionMet && triesLeft-- > 0); } @Test - public void testModulesCompletionInParent() throws IOException, URISyntaxException { + public void testModulesCompletionInParent() throws IOException, URISyntaxException, InterruptedException { // We need the WORKSPACE projects to be placed to MavenProjectCache IWorkspaceServiceParticipant workspaceService = languageService.getWorkspaceServiceParticipants().stream().filter(MavenWorkspaceService.class::isInstance).findAny().get(); assertNotNull(workspaceService); @@ -395,32 +470,57 @@ public void testModulesCompletionInParent() throws IOException, URISyntaxExcepti Arrays.asList(new WorkspaceFolder[] {wsFolder}), Arrays.asList(new WorkspaceFolder[0])))); + DOMDocument documentA = createDOMDocument("/modules/module-a-pom.xml", languageService); + languageService.didOpen(documentA); + List diagnosticsA = languageService.doDiagnostics( - createDOMDocument("/modules/module-a-pom.xml", languageService), - new XMLValidationSettings(), Map.of(), () -> {}); + documentA, new XMLValidationSettings(), Map.of(), () -> {}); assertFalse(diagnosticsA.stream().anyMatch(diag -> (diag.getMessage().contains("ModuleA") || diag.getMessage().contains("ModuleB")))); - DOMDocument document = createDOMDocument("/modules/dependent/module-c-pom.xml", languageService); + DOMDocument documentC = createDOMDocument("/modules/dependent/module-c-pom.xml", languageService); + languageService.didOpen(documentC); + List diagnosticsC = languageService.doDiagnostics( - document, - new XMLValidationSettings(), Map.of(), () -> {}); + documentC, new XMLValidationSettings(), Map.of(), () -> {}); assertFalse(diagnosticsC.stream().anyMatch(diag -> (diag.getMessage().contains("ModuleA") || diag.getMessage().contains("ModuleB")))); // in // for group ID - List completions = languageService.doComplete(document, new Position(9, 13), new SharedSettings()).getItems(); - assertTrue(completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) - .anyMatch("org.test.modules"::equals)); - + List completions = null; + int triesLeft = 15; // given a 1-second `sleep` between the retries this gives >15 seconds overall timeout + boolean conditionMet = false; + do { + completions = languageService.doComplete( + documentC, new Position(9, 13), new SharedSettings()) + .getItems(); + Thread.sleep(1000); + conditionMet = completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) + .anyMatch("org.test.modules"::equals); + } while (!conditionMet && triesLeft-- > 0); + // for artifact ID: - completions = languageService.doComplete(document, new Position(10, 16), new SharedSettings()).getItems(); - assertTrue(completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) - .anyMatch("ModuleA"::equals)); + triesLeft = 15; // given a 1-second `sleep` between the retries this gives >15 seconds overall timeout + conditionMet = false; + do { + completions = languageService.doComplete( + documentC, new Position(10, 16), new SharedSettings()) + .getItems(); + Thread.sleep(1000); + conditionMet = completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) + .anyMatch("ModuleA"::equals); + } while (!conditionMet && triesLeft-- > 0); // for versions - completions = languageService.doComplete(document, new Position(11, 13), new SharedSettings()).getItems(); - assertTrue(completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) - .anyMatch("0.0.1-SNAPSHOT"::equals)); + triesLeft = 15; // given a 1-second `sleep` between the retries this gives >15 seconds overall timeout + conditionMet = false; + do { + completions = languageService.doComplete( + documentC, new Position(11, 13), new SharedSettings()) + .getItems(); + Thread.sleep(1000); + conditionMet = completions.stream().map(CompletionItem::getTextEdit).map(Either::getLeft).map(TextEdit::getNewText) + .anyMatch("0.0.1-SNAPSHOT"::equals); + } while (!conditionMet && triesLeft-- > 0); } @Test @@ -440,12 +540,16 @@ public void testModulesDefinitionInDependency() throws IOException, URISyntaxExc Arrays.asList(new WorkspaceFolder[0])))); DOMDocument document = createDOMDocument("/modules/dependent/module-b-pom.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(11, 18); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); assertFalse(definitionLinks.isEmpty()); DOMDocument targetDocument = createDOMDocument("/modules/module-a-pom.xml", languageService); + languageService.didOpen(targetDocument); + assertTrue(definitionLinks.stream().anyMatch(link -> link.getTargetUri().equals(targetDocument.getDocumentURI()))); } @@ -466,12 +570,16 @@ public void testModulesDefinitionInParent() throws IOException, URISyntaxExcepti Arrays.asList(new WorkspaceFolder[0])))); DOMDocument document = createDOMDocument("/modules/dependent/module-c-pom.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(10, 16); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); assertFalse(definitionLinks.isEmpty()); DOMDocument targetDocument = createDOMDocument("/modules/module-a-pom.xml", languageService); + languageService.didOpen(targetDocument); + assertTrue(definitionLinks.stream().anyMatch(link -> link.getTargetUri().equals(targetDocument.getDocumentURI()))); } @@ -491,6 +599,8 @@ public void testModulesHoverInDependency() throws IOException, InterruptedExcept Arrays.asList(new WorkspaceFolder[0])))); DOMDocument document = createDOMDocument("/modules/dependent/module-b-pom.xml", languageService); + languageService.didOpen(document); + // Hover over groupID text Position pos = new Position(10, 16); // o|rg.test.modules @@ -524,7 +634,8 @@ public void testModulesHoverInParent() throws IOException, InterruptedException, Arrays.asList(new WorkspaceFolder[0])))); DOMDocument document = createDOMDocument("/modules/dependent/module-c-pom.xml", languageService); - + languageService.didOpen(document); + // Hover over groupID text Position pos = new Position(9, 14); // o|rg.test.modules Hover hover = languageService.doHover(document, pos, new SharedSettings()); @@ -544,34 +655,46 @@ public void testModulesHoverInParent() throws IOException, InterruptedException, @Test public void testParentDefinitionWithRelativePath() throws IOException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-with-properties-in-parent-for-definition.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(6, 9); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); DOMDocument targetDocument = createDOMDocument("/pom-with-properties-for-definition.xml", languageService); + languageService.didOpen(targetDocument); + assertTrue(definitionLinks.stream().anyMatch(link -> link.getTargetUri().equals(targetDocument.getDocumentURI()))); } @Test public void testParentDefinitionWithoutRelativePath() throws IOException, URISyntaxException { DOMDocument document = createDOMDocument("/multi-module/folder1/pom.xml", languageService); + languageService.didOpen(document); + Position pos = new Position(6, 9); List definitionLinks = languageService.findDefinition(document, pos, () -> { }); DOMDocument targetDocument = createDOMDocument("/multi-module/pom.xml", languageService); + languageService.didOpen(targetDocument); + assertTrue(definitionLinks.stream().anyMatch(link -> link.getTargetUri().equals(targetDocument.getDocumentURI()))); } @Test public void testBOMDependency() throws IOException, URISyntaxException { DOMDocument document = createDOMDocument("/pom-bom.xml", languageService); + languageService.didOpen(document); + assertEquals(Collections.emptyList(), languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), () -> {})); } @Test public void testCompleteSNAPSHOT() throws Exception { DOMDocument document = createDOMDocument("/pom-version.xml", languageService); + languageService.didOpen(document); + Optional edit = languageService.doComplete(document, new Position(0, 11), new SharedSettings()).getItems().stream().map(CompletionItem::getTextEdit).map(Either::getLeft).findFirst(); assertTrue(edit.isPresent()); assertEquals("-SNAPSHOT", edit.get().getNewText()); @@ -581,6 +704,8 @@ public void testCompleteSNAPSHOT() throws Exception { @Test public void testResolveParentFromCentralWhenAnotherRepoIsDeclared() throws Exception { DOMDocument document = createDOMDocument("/it1/pom.xml", languageService); + languageService.didOpen(document); + assertArrayEquals(new Diagnostic[] { // // org.sonatype.forge:forge-parent:jar:10 failed to transfer from https://download.eclipse.org/eclipse/updates/4.16/ // during a previous attempt. This failure was cached in the local repository and resolution is not reattempted @@ -591,8 +716,8 @@ public void testResolveParentFromCentralWhenAnotherRepoIsDeclared() throws Excep // during a previous attempt. This failure was cached in the local repository and resolution is // not reattempted until the update interval of central has elapsed or updates are forced d(27, 0, 31, 13, null, "", "xml", DiagnosticSeverity.Error) // - } - , languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), () -> {}) + }, + languageService.doDiagnostics(document, new XMLValidationSettings(), Map.of(), () -> {}) .stream() .filter(diag -> { // as message is to complex, we don't compare them @@ -601,18 +726,14 @@ public void testResolveParentFromCentralWhenAnotherRepoIsDeclared() throws Excep }) .toArray(Diagnostic[]::new)); } - - @Test public void testSystemPath() throws Exception { - // We create an instance here of language service to be sure that workspace - // folders will be not filled by an another test - MavenLanguageService languageService = new MavenLanguageService(); + DOMDocument document = createDOMDocument("/pom-systemPath.xml", languageService); + languageService.didOpen(document); + List diagnostics = languageService.doDiagnostics( - createDOMDocument("/pom-systemPath.xml", languageService), new XMLValidationSettings(), Map.of(), - () -> { - }); + document, new XMLValidationSettings(), Map.of(), () -> {}); // 'dependencies.dependency.systemPath' for a:a:jar should not point at files // within the project directory, ${basedir} will be unresolvable by dependent // projects @@ -627,6 +748,8 @@ public void testSystemPath() throws Exception { @Test public void testPluginInProfileOnly() throws Exception { DOMDocument document = createDOMDocument("/pom-gpg.xml", languageService); + languageService.didOpen(document); + Optional diagnostics = languageService.doDiagnostics( document, new XMLValidationSettings(), Map.of(), () -> {}).stream().filter(diag -> diag.getSeverity() == DiagnosticSeverity.Warning).findAny(); assertTrue(diagnostics.isEmpty(), () -> diagnostics.map(Object::toString).get()); @@ -640,7 +763,10 @@ public void testParentAsWorkspaceFolderInInitializeParam() throws Exception { new WorkspaceFolder(MavenLemminxTestsUtils.class.getResource(childFolder).toURI().toString()), new WorkspaceFolder(MavenLemminxTestsUtils.class.getResource("/parentAsSiblingProjectWithoutRelativePath/parent").toURI().toString()))); languageService.initializeParams(params); + DOMDocument document = createDOMDocument(childFolder + "/pom.xml", languageService); + languageService.didOpen(document); + Optional diagnostics = languageService.doDiagnostics( document, new XMLValidationSettings(), Map.of(), () -> {}).stream().findAny(); assertTrue(diagnostics.isEmpty(), () -> diagnostics.map(Object::toString).get()); diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipantTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipantTest.java index 1baf93e0..c7a653fa 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipantTest.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipantTest.java @@ -31,7 +31,6 @@ import org.eclipse.lemminx.extensions.maven.MavenWorkspaceService; import org.eclipse.lemminx.extensions.maven.NoMavenCentralExtension; import org.eclipse.lemminx.extensions.maven.utils.DOMUtils; -import org.eclipse.lemminx.services.XMLLanguageService; import org.eclipse.lemminx.services.extensions.IWorkspaceServiceParticipant; import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; import org.eclipse.lsp4j.Position; @@ -47,14 +46,15 @@ @ExtendWith(NoMavenCentralExtension.class) public class MavenPropertyRenameParticipantTest { - private XMLLanguageService xmlLanguageService = new MavenLanguageService(); + private MavenLanguageService languageService = new MavenLanguageService(); @Test public void testRenameMavenProperty() throws Exception { String propertyName = "myPropertyGroupId"; String newPropertyName = "new-group-id"; - DOMDocument xmlDocument = createDOMDocument("/property-refactoring/pom-with-property.xml", xmlLanguageService); - + DOMDocument xmlDocument = createDOMDocument("/property-refactoring/pom-with-property.xml", languageService); + languageService.didOpen(xmlDocument); + Optional properties = DOMUtils.findChildElement(xmlDocument.getDocumentElement(), PROPERTIES_ELT); assertTrue(properties.isPresent(), "'' Element doesn't exist!"); @@ -93,14 +93,14 @@ public void testRenameMavenProperty() throws Exception { Position startTagMiddle = xmlDocument.positionAt((startTagOpenOffset + startTagCloseOffset) / 2); Either prepareResult = - xmlLanguageService.prepareRename(xmlDocument, startTagMiddle, () -> {}); + languageService.prepareRename(xmlDocument, startTagMiddle, () -> {}); assertNotNull(prepareResult, "Prepare Result is null!"); assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); Range prepareResultRange = prepareResult.getLeft(); assertEquals(expectedStartTagRange, prepareResultRange); - WorkspaceEdit renameReult = xmlLanguageService.doRename(xmlDocument, startTagMiddle, newPropertyName, () -> {}); + WorkspaceEdit renameReult = languageService.doRename(xmlDocument, startTagMiddle, newPropertyName, () -> {}); assertNotNull(renameReult, "Prepare Result is null!"); assertNotNull(renameReult.getDocumentChanges(), "Rename result document changes is null!"); assertEquals(expectedRenameResult, renameReult); @@ -108,14 +108,14 @@ public void testRenameMavenProperty() throws Exception { // Test renaming end tag of maven property definition Position endTagMiddle = xmlDocument.positionAt((endTagOpenOffset + endTagCloseOffset) / 2); - prepareResult = xmlLanguageService.prepareRename(xmlDocument, endTagMiddle, () -> {}); + prepareResult = languageService.prepareRename(xmlDocument, endTagMiddle, () -> {}); assertNotNull(prepareResult, "Prepare Result is null!"); assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); prepareResultRange = prepareResult.getLeft(); assertEquals(expectedEndTagRange, prepareResultRange); - renameReult = xmlLanguageService.doRename(xmlDocument, endTagMiddle, newPropertyName, () -> {}); + renameReult = languageService.doRename(xmlDocument, endTagMiddle, newPropertyName, () -> {}); assertNotNull(renameReult, "Prepare Result is null!"); assertNotNull(renameReult.getDocumentChanges(), "Rename result document changes is null!"); assertEquals(expectedRenameResult, renameReult); @@ -126,14 +126,14 @@ public void testRenameMavenProperty() throws Exception { (firstUse.getStart().getLine() + firstUse.getEnd().getLine()) / 2, (firstUse.getStart().getCharacter() + firstUse.getEnd().getCharacter()) / 2); - prepareResult = xmlLanguageService.prepareRename(xmlDocument, firstUseMiddle, () -> {}); + prepareResult = languageService.prepareRename(xmlDocument, firstUseMiddle, () -> {}); assertNotNull(prepareResult, "Prepare Result is null!"); assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); prepareResultRange = prepareResult.getLeft(); assertEquals(expectedFirstUseRange, prepareResultRange); - renameReult = xmlLanguageService.doRename(xmlDocument, firstUseMiddle, newPropertyName, () -> {}); + renameReult = languageService.doRename(xmlDocument, firstUseMiddle, newPropertyName, () -> {}); assertNotNull(renameReult, "Prepare Result is null!"); assertNotNull(renameReult.getDocumentChanges(), "Rename result document changes is null!"); assertEquals(expectedRenameResult, renameReult); @@ -142,7 +142,7 @@ public void testRenameMavenProperty() throws Exception { @Test public void testRenameMavenPropertyWithChildren() throws Exception { // We need the WORKSPACE projects to be placed to MavenProjectCache - IWorkspaceServiceParticipant workspaceService = xmlLanguageService.getWorkspaceServiceParticipants().stream().filter(MavenWorkspaceService.class::isInstance).findAny().get(); + IWorkspaceServiceParticipant workspaceService = languageService.getWorkspaceServiceParticipants().stream().filter(MavenWorkspaceService.class::isInstance).findAny().get(); assertNotNull(workspaceService); URI folderUri = getClass().getResource("/property-refactoring/child").toURI(); @@ -157,7 +157,9 @@ public void testRenameMavenPropertyWithChildren() throws Exception { String propertyName = "test-version"; String newPropertyName = "new-test-version"; - DOMDocument xmlDocument = createDOMDocument("/property-refactoring/child/parent/pom.xml", xmlLanguageService); + DOMDocument xmlDocument = createDOMDocument("/property-refactoring/child/parent/pom.xml", languageService); + languageService.didOpen(xmlDocument); + assertNotNull(xmlDocument, "Parent document not found!"); Optional properties = DOMUtils.findChildElement(xmlDocument.getDocumentElement(), PROPERTIES_ELT); @@ -183,7 +185,9 @@ public void testRenameMavenPropertyWithChildren() throws Exception { Range expectedFirstUseRange = propertyUseRanges.get(0); // Save property use ranges for child document - DOMDocument childXmlDocument = createDOMDocument("/property-refactoring/child/pom.xml", xmlLanguageService); + DOMDocument childXmlDocument = createDOMDocument("/property-refactoring/child/pom.xml", languageService); + languageService.didOpen(xmlDocument); + assertNotNull(childXmlDocument, "Child document not found!"); List childPropertyUseRanges =collectMavenPropertyUsages(childXmlDocument, propertyName); @@ -209,14 +213,14 @@ public void testRenameMavenPropertyWithChildren() throws Exception { Position startTagMiddle = xmlDocument.positionAt((startTagOpenOffset + startTagCloseOffset) / 2); Either prepareResult = - xmlLanguageService.prepareRename(xmlDocument, startTagMiddle, () -> {}); + languageService.prepareRename(xmlDocument, startTagMiddle, () -> {}); assertNotNull(prepareResult, "Prepare Result is null!"); assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); Range prepareResultRange = prepareResult.getLeft(); assertEquals(expectedStartTagRange, prepareResultRange); - WorkspaceEdit renameReult = xmlLanguageService.doRename(xmlDocument, startTagMiddle, newPropertyName, () -> {}); + WorkspaceEdit renameReult = languageService.doRename(xmlDocument, startTagMiddle, newPropertyName, () -> {}); assertNotNull(renameReult, "Prepare Result is null!"); assertNotNull(renameReult.getDocumentChanges(), "Rename result document changes is null!"); assertEquals(expectedRenameResult, renameReult); @@ -224,14 +228,14 @@ public void testRenameMavenPropertyWithChildren() throws Exception { // Test renaming end tag of maven property definition Position endTagMiddle = xmlDocument.positionAt((endTagOpenOffset + endTagCloseOffset) / 2); - prepareResult = xmlLanguageService.prepareRename(xmlDocument, endTagMiddle, () -> {}); + prepareResult = languageService.prepareRename(xmlDocument, endTagMiddle, () -> {}); assertNotNull(prepareResult, "Prepare Result is null!"); assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); prepareResultRange = prepareResult.getLeft(); assertEquals(expectedEndTagRange, prepareResultRange); - renameReult = xmlLanguageService.doRename(xmlDocument, endTagMiddle, newPropertyName, () -> {}); + renameReult = languageService.doRename(xmlDocument, endTagMiddle, newPropertyName, () -> {}); assertNotNull(renameReult, "Prepare Result is null!"); assertNotNull(renameReult.getDocumentChanges(), "Rename result document changes is null!"); assertEquals(expectedRenameResult, renameReult); @@ -242,14 +246,14 @@ public void testRenameMavenPropertyWithChildren() throws Exception { (firstUse.getStart().getLine() + firstUse.getEnd().getLine()) / 2, (firstUse.getStart().getCharacter() + firstUse.getEnd().getCharacter()) / 2); - prepareResult = xmlLanguageService.prepareRename(xmlDocument, firstUseMiddle, () -> {}); + prepareResult = languageService.prepareRename(xmlDocument, firstUseMiddle, () -> {}); assertNotNull(prepareResult, "Prepare Result is null!"); assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); prepareResultRange = prepareResult.getLeft(); assertEquals(expectedFirstUseRange, prepareResultRange); - renameReult = xmlLanguageService.doRename(xmlDocument, firstUseMiddle, newPropertyName, () -> {}); + renameReult = languageService.doRename(xmlDocument, firstUseMiddle, newPropertyName, () -> {}); assertNotNull(renameReult, "Prepare Result is null!"); assertNotNull(renameReult.getDocumentChanges(), "Rename result document changes is null!"); assertEquals(expectedRenameResult, renameReult); diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/MavenLemminxTestsUtils.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/MavenLemminxTestsUtils.java index ff35ec57..0dc7e4d4 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/MavenLemminxTestsUtils.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/MavenLemminxTestsUtils.java @@ -53,7 +53,6 @@ public void checkCanceled() { throw new CancellationException("Call is cancelled on phase " + cancellingPhase); } } - } // Counts the 'checkCanceled()` calls for an operation @@ -68,13 +67,16 @@ public void checkCanceled() { public int getCounterValue() { return counter; } - } public static TextDocumentItem createTextDocumentItem(String resourcePath) throws IOException, URISyntaxException { return createTextDocumentItem(resourcePath, null); } - + + public static TextDocumentItem createTextDocumentItem(URI uri) throws IOException, URISyntaxException { + return createTextDocumentItem(uri, null); + } + public static DOMDocument createDOMDocument(String resourcePath, Properties replacements, XMLLanguageService languageService) throws IOException, URISyntaxException { return org.eclipse.lemminx.dom.DOMParser.getInstance().parse(new TextDocument(createTextDocumentItem(resourcePath, replacements)), languageService.getResolverExtensionManager()); } @@ -83,8 +85,16 @@ public static DOMDocument createDOMDocument(String resourcePath, XMLLanguageServ return org.eclipse.lemminx.dom.DOMParser.getInstance().parse(new TextDocument(createTextDocumentItem(resourcePath)), languageService.getResolverExtensionManager()); } + public static DOMDocument createDOMDocument(URI uri, XMLLanguageService languageService) throws IOException, URISyntaxException { + return org.eclipse.lemminx.dom.DOMParser.getInstance().parse(new TextDocument(createTextDocumentItem(uri)), languageService.getResolverExtensionManager()); + } + public static TextDocumentItem createTextDocumentItem(String resourcePath, Properties replacements) throws IOException, URISyntaxException { URI uri = MavenLemminxTestsUtils.class.getResource(resourcePath).toURI(); + return createTextDocumentItem(uri, replacements); + } + + public static TextDocumentItem createTextDocumentItem(URI uri, Properties replacements) throws IOException, URISyntaxException { File file = new File(uri); String contents = Files.readString(file.toPath()); if (replacements != null) { diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/ParticipantUtilsTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/ParticipantUtilsTest.java index e61785c3..a239e4e1 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/ParticipantUtilsTest.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/ParticipantUtilsTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 Red Hat Inc. and others. + * Copyright (c) 2022, 2023 Red Hat Inc. and others. * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ @@ -30,7 +30,6 @@ import org.eclipse.lemminx.extensions.maven.MavenLemminxExtension; import org.eclipse.lemminx.extensions.maven.MavenProjectCache; import org.eclipse.lemminx.extensions.maven.NoMavenCentralExtension; -import org.eclipse.lemminx.services.XMLLanguageService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,7 +37,7 @@ @ExtendWith(NoMavenCentralExtension.class) public class ParticipantUtilsTest { - private XMLLanguageService languageService; + private MavenLanguageService languageService; @BeforeEach public void setUp() throws IOException { @@ -53,8 +52,12 @@ public void tearDown() throws InterruptedException, ExecutionException { @Test public void testResolveValueWithProperties() throws IOException, URISyntaxException { - DOMDocument document = createDOMDocument("/pom-with-properties.xml", languageService); MavenLemminxExtension plugin = new MavenLemminxExtension(); + plugin.start(null,languageService); + + DOMDocument document = createDOMDocument("/pom-with-properties.xml", languageService); + languageService.didOpen(document); + MavenProjectCache cache = plugin.getProjectCache(); MavenProject project = cache.getLastSuccessfulMavenProject(document); assertNotNull(project);