diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/BuildPathCommand.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/BuildPathCommand.java index 59fa3d3f89..78c0864ad2 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/BuildPathCommand.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/BuildPathCommand.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Microsoft Corporation and others. + * Copyright (c) 2018-2021 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -13,6 +13,7 @@ package org.eclipse.jdt.ls.core.internal.commands; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.List; @@ -81,7 +82,9 @@ public static Result addToSourcePath(String sourceFolderUri) { IJavaProject javaProject = JavaCore.create(targetProject); try { if (ProjectUtils.addSourcePath(sourcePath, exclusionPath, javaProject)) { - return new Result(true, Messages.format("Successfully added ''{0}'' to the project {1}''s source path.", new String[] { getWorkspacePath(sourceFolderPath).toOSString(), targetProject.getName() })); + Result result = new Result(true, Messages.format("Successfully added ''{0}'' to the project {1}''s source path.", new String[] { getWorkspacePath(sourceFolderPath).toOSString(), targetProject.getName() })); + result.sourcePaths = getInvisibleProjectRelativeSourcePaths(javaProject); + return result; } else { return new Result(true, Messages.format("No need to add it to source path again, because the folder ''{0}'' is already in the project {1}''s source path.", new String[] { getWorkspacePath(sourceFolderPath).toOSString(), targetProject.getName() })); @@ -126,7 +129,9 @@ public static Result removeFromSourcePath(String sourceFolderUri) { IJavaProject javaProject = JavaCore.create(targetProject); try { if (ProjectUtils.removeSourcePath(sourcePath, javaProject)) { - return new Result(true, Messages.format("Successfully removed ''{0}'' from the project {1}''s source path.", new String[] { getWorkspacePath(sourceFolderPath).toOSString(), targetProject.getName() })); + Result result = new Result(true, Messages.format("Successfully removed ''{0}'' from the project {1}''s source path.", new String[] { getWorkspacePath(sourceFolderPath).toOSString(), targetProject.getName() })); + result.sourcePaths = getInvisibleProjectRelativeSourcePaths(javaProject); + return result; } else { return new Result(true, Messages.format("No need to remove it from source path, because the folder ''{0}'' isn''t on any project''s source path.", getWorkspacePath(sourceFolderPath).toOSString())); } @@ -184,6 +189,20 @@ public static Result listSourcePaths() { return new ListCommandResult(true, null, sourcePathList.toArray(new SourcePath[0])); } + private static String[] getInvisibleProjectRelativeSourcePaths(IJavaProject javaProject) throws JavaModelException { + if (ProjectUtils.isVisibleProject(javaProject.getProject())) { + return null; + } + IFolder workspaceLinkFolder = javaProject.getProject().getFolder(ProjectUtils.WORKSPACE_LINK); + if (!workspaceLinkFolder.isLinked()) { + return null; + } + IPath[] paths = ProjectUtils.listSourcePaths(javaProject); + return Arrays.stream(paths) + .map(p -> p.makeRelativeTo(workspaceLinkFolder.getFullPath()).toString()) + .toArray(String[]::new); + } + private static IProject findBelongedProject(IPath sourceFolder) { List projects = Stream.of(ProjectUtils.getAllProjects()).filter(ProjectUtils::isJavaProject).sorted(new Comparator() { @Override @@ -218,6 +237,7 @@ private static IPath getWorkspacePath(IPath path) { public static class Result { public boolean status; public String message; + public String[] sourcePaths; Result(boolean status, String message) { this.status = status; diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentLifeCycleHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentLifeCycleHandler.java index c2f5bdf032..fa9a48c0f9 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentLifeCycleHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentLifeCycleHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016-2017 Red Hat Inc. and others. + * Copyright (c) 2016-2021 Red Hat Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -17,7 +17,9 @@ import java.util.Optional; import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.manipulation.CoreASTProvider; @@ -79,7 +81,11 @@ public ICompilationUnit resolveCompilationUnit(String uri) { boolean invisibleProjectEnabled = false; if (belongedRootPath.isPresent()) { IPath rootPath = belongedRootPath.get(); - invisibleProjectEnabled = InvisibleProjectImporter.loadInvisibleProject(filePath, rootPath, false); + try { + invisibleProjectEnabled = InvisibleProjectImporter.loadInvisibleProject(filePath, rootPath, false, new NullProgressMonitor()); + } catch (CoreException e) { + JavaLanguageServerPlugin.logException("Failed to load invisible project", e); + } if (invisibleProjectEnabled) { unit = JDTUtils.resolveCompilationUnit(uri); } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectImporter.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectImporter.java index 5cd92d9f3c..be5d0ed76f 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectImporter.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectImporter.java @@ -21,6 +21,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -35,14 +37,12 @@ import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.ls.core.internal.AbstractProjectImporter; import org.eclipse.jdt.ls.core.internal.IConstants; import org.eclipse.jdt.ls.core.internal.JDTUtils; @@ -91,7 +91,7 @@ public void importToWorkspace(IProgressMonitor monitor) throws OperationCanceled return; } - loadInvisibleProject(triggerJavaFile.get(), rootPath, true); + loadInvisibleProject(triggerJavaFile.get(), rootPath, true, monitor); } @Override @@ -99,12 +99,127 @@ public void reset() { // do nothing } - public static boolean setInvisibleProjectOutputPath(IJavaProject javaProject, String outputPath, boolean isUpdate, IProgressMonitor monitor) throws CoreException { - IProject project = javaProject.getProject(); - if (ProjectUtils.isVisibleProject(project)) { + /** + * Based on the trigger file, check whether to load the invisible project to + * manage it. Return true if an invisible project is enabled. + * + * @throws CoreException + */ + public static boolean loadInvisibleProject(IPath javaFile, IPath rootPath, boolean forceUpdateLibPath, IProgressMonitor monitor) throws CoreException { + if (!ProjectUtils.getVisibleProjects(rootPath).isEmpty()) { return false; } - + + String packageName = getPackageName(javaFile, rootPath); + IPath sourceDirectory = inferSourceDirectory(javaFile.toFile().toPath(), packageName); + if (sourceDirectory == null || !rootPath.isPrefixOf(sourceDirectory) + || isPartOfMatureProject(sourceDirectory)) { + return false; + } + + String invisibleProjectName = ProjectUtils.getWorkspaceInvisibleProjectName(rootPath); + IProject invisibleProject = ResourcesPlugin.getWorkspace().getRoot().getProject(invisibleProjectName); + + if (!invisibleProject.exists()) { + try { + JavaLanguageServerPlugin.logInfo("Try to create an invisible project for the workspace " + rootPath); + invisibleProject = ProjectUtils.createInvisibleProjectIfNotExist(rootPath); + forceUpdateLibPath = true; + JavaLanguageServerPlugin.logInfo("Successfully created a workspace invisible project " + invisibleProjectName); + } catch (CoreException e) { + JavaLanguageServerPlugin.logException("Failed to create the invisible project.", e); + return false; + } + } + + IJavaProject javaProject = JavaCore.create(invisibleProject); + + IFolder workspaceLinkFolder = invisibleProject.getFolder(ProjectUtils.WORKSPACE_LINK); + PreferenceManager preferencesManager = JavaLanguageServerPlugin.getPreferencesManager(); + List sourcePathsFromPreferences = null; + if (preferencesManager != null && preferencesManager.getPreferences() != null) { + sourcePathsFromPreferences = preferencesManager.getPreferences().getInvisibleProjectSourcePaths(); + } + List sourcePaths; + if (sourcePathsFromPreferences != null) { + sourcePaths = getSourcePaths(sourcePathsFromPreferences, workspaceLinkFolder); + } else { + IPath relativeSourcePath = sourceDirectory.makeRelativeTo(rootPath); + IPath sourcePath = workspaceLinkFolder.getFolder(relativeSourcePath).getFullPath(); + sourcePaths = Arrays.asList(sourcePath); + } + + List excludingPaths = getExcludingPath(javaProject, rootPath, workspaceLinkFolder); + + IPath outputPath = getOutputPath(javaProject, preferencesManager.getPreferences().getInvisibleProjectOutputPath(), false /*isUpdate*/); + + IClasspathEntry[] classpathEntries = resolveClassPathEntries(javaProject, sourcePaths, excludingPaths, outputPath); + javaProject.setRawClasspath(classpathEntries, outputPath, monitor); + + if (forceUpdateLibPath && preferencesManager != null && preferencesManager.getPreferences() != null) { + UpdateClasspathJob.getInstance().updateClasspath(javaProject, preferencesManager.getPreferences().getReferencedLibraries()); + } + + return true; + } + + + public static IClasspathEntry[] resolveClassPathEntries(IJavaProject javaProject, List sourcePaths, List excludingPaths, IPath outputPath) throws CoreException { + List newEntries = new LinkedList<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) { + newEntries.add(entry); + } + } + + // Sort the source paths to make the child folders come first + Collections.sort(sourcePaths, new Comparator() { + @Override + public int compare(IPath path1, IPath path2) { + return path1.toString().compareTo(path2.toString()) * -1; + } + }); + + List sourceEntries = new LinkedList<>(); + for (IPath currentPath : sourcePaths) { + boolean canAddToSourceEntries = true; + List exclusionPatterns = new ArrayList<>(); + for (IClasspathEntry sourceEntry : sourceEntries) { + if (Objects.equals(sourceEntry.getPath(), currentPath)) { + JavaLanguageServerPlugin.logError("Skip duplicated source path: " + currentPath.toString()); + canAddToSourceEntries = false; + break; + } + + if (currentPath.isPrefixOf(sourceEntry.getPath())) { + exclusionPatterns.add(sourceEntry.getPath().makeRelativeTo(currentPath).addTrailingSeparator()); + } + } + + if (currentPath.equals(outputPath)) { + throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "The output path cannot be equal to the source path, please provide a new path.")); + } else if (currentPath.isPrefixOf(outputPath)) { + exclusionPatterns.add(outputPath.makeRelativeTo(currentPath).addTrailingSeparator()); + } else if (outputPath.isPrefixOf(currentPath)) { + throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "The specified output path contains source folders, please provide a new path instead.")); + } + + if (canAddToSourceEntries) { + if (excludingPaths != null) { + for (IPath exclusion : excludingPaths) { + if (currentPath.isPrefixOf(exclusion) && !currentPath.equals(exclusion)) { + exclusionPatterns.add(exclusion.makeRelativeTo(currentPath).addTrailingSeparator()); + } + } + } + sourceEntries.add(JavaCore.newSourceEntry(currentPath, exclusionPatterns.toArray(IPath[]::new))); + } + } + newEntries.addAll(sourceEntries); + return newEntries.toArray(IClasspathEntry[]::new); + } + + public static IPath getOutputPath(IJavaProject javaProject, String outputPath, boolean isUpdate) throws CoreException { if (outputPath == null) { outputPath = ""; } else { @@ -114,120 +229,67 @@ public static boolean setInvisibleProjectOutputPath(IJavaProject javaProject, St if (new org.eclipse.core.runtime.Path(outputPath).isAbsolute()) { throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "The output path must be a relative path to the workspace.")); } - + + IProject project = javaProject.getProject(); if (StringUtils.isEmpty(outputPath)) { // blank means using default output path - javaProject.setOutputLocation(project.getFolder("bin").getFullPath(), monitor); - return true; + return javaProject.getProject().getFolder("bin").getFullPath(); } outputPath = ProjectUtils.WORKSPACE_LINK + IPath.SEPARATOR + outputPath; IPath outputFullPath = project.getFolder(outputPath).getFullPath(); if (javaProject.getOutputLocation().equals(outputFullPath)) { - return false; + return outputFullPath; } File outputDirectory = project.getFolder(outputPath).getLocation().toFile(); - // Avoid popping too much dialog during activation, only show error dialog when it's updated + // Avoid popping too much dialogs during activation, only show the error dialog when it's updated if (isUpdate && outputDirectory.exists() && outputDirectory.list().length != 0) { throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "Cannot set the output path to a folder which is not empty, please provide a new path.")); } - IClasspathEntry[] existingEntries = javaProject.getRawClasspath(); - List newEntries = new ArrayList<>(); - for (IClasspathEntry entry : existingEntries) { - if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) { - newEntries.add(entry); - } else { - IPath entryPath = entry.getPath(); - if (entryPath.equals(outputFullPath)) { - throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, - "The output path cannot be equal to the source folder path, please provide a new path.")); - } else if (entryPath.isPrefixOf(outputFullPath)) { - List exclusionPatterns = new ArrayList<>(Arrays.asList(entry.getExclusionPatterns())); - IPath excludePath = outputFullPath.makeRelativeTo(entryPath); - boolean hasExcluded = false; - for (IPath exclusionPattern : exclusionPatterns) { - if (exclusionPattern.isPrefixOf(excludePath)) { - hasExcluded = true; - break; - } - } - if (!hasExcluded) { - exclusionPatterns.add(excludePath.addTrailingSeparator()); - } - newEntries.add(JavaCore.newSourceEntry(entryPath, exclusionPatterns.toArray(new IPath[0]))); - continue; - } else if (outputFullPath.isPrefixOf(entryPath)) { - throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, - "The specified output path contains source folders, please provide a new path instead.")); - } else { - newEntries.add(entry); - } - } - } - - try { - javaProject.setRawClasspath(newEntries.toArray(new IClasspathEntry[0]), outputFullPath, monitor); - } catch (JavaModelException e) { - throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "Failed to set the output path.", e)); - } - return true; + return outputFullPath; } - /** - * Based on the trigger file, check whether to load the invisible project to manage it. - * Return true if an invisible project is enabled. - */ - public static boolean loadInvisibleProject(IPath javaFile, IPath rootPath, boolean forceUpdateLibPath) { - if (!ProjectUtils.getVisibleProjects(rootPath).isEmpty()) { - return false; + public static List getSourcePaths(List sourcePaths, IFolder workspaceLinkFolder) throws CoreException { + if (sourcePaths == null) { + return Collections.emptyList(); } - String packageName = getPackageName(javaFile, rootPath); - IPath sourceDirectory = inferSourceDirectory(javaFile.toFile().toPath(), packageName); - if (sourceDirectory == null || !rootPath.isPrefixOf(sourceDirectory) - || isPartOfMatureProject(sourceDirectory)) { - return false; - } + sourcePaths = sourcePaths.stream() + .map(path -> path.trim()) + .distinct() + .collect(Collectors.toList()); - String invisibleProjectName = ProjectUtils.getWorkspaceInvisibleProjectName(rootPath); - IProject invisibleProject = ResourcesPlugin.getWorkspace().getRoot().getProject(invisibleProjectName); - IPath libFolder = new Path(InvisibleProjectBuildSupport.LIB_FOLDER); - IFolder workspaceLinkFolder = invisibleProject.getFolder(ProjectUtils.WORKSPACE_LINK); - IPath relativeSourcePath = sourceDirectory.makeRelativeTo(rootPath); - IPath sourcePath = relativeSourcePath.isEmpty() ? workspaceLinkFolder.getFullPath() : workspaceLinkFolder.getFolder(relativeSourcePath).getFullPath(); - if (!invisibleProject.exists()) { - try { - JavaLanguageServerPlugin.logInfo("Try to create an invisible project for the workspace " + rootPath); - invisibleProject = ProjectUtils.createInvisibleProjectIfNotExist(rootPath); - List subProjects = ProjectUtils.getVisibleProjects(rootPath); - List subProjectPaths = subProjects.stream().map(project -> { - IPath relativePath = project.getLocation().makeRelativeTo(rootPath); - return workspaceLinkFolder.getFolder(relativePath).getFullPath(); - }).collect(Collectors.toList()); - subProjectPaths.add(libFolder); - IJavaProject javaProject = JavaCore.create(invisibleProject); - ProjectUtils.addSourcePath(sourcePath, subProjectPaths.toArray(new IPath[0]), javaProject); - setInvisibleProjectOutputPath(javaProject, JavaLanguageServerPlugin.getPreferencesManager().getPreferences().getInvisibleProjectOutputPath(), - false /*isUpdate*/, new NullProgressMonitor()); - JavaLanguageServerPlugin.logInfo("Successfully created a workspace invisible project " + invisibleProjectName); - forceUpdateLibPath = true; - } catch (CoreException e) { - JavaLanguageServerPlugin.logException("Failed to create the invisible project.", e); - return false; + List sourceList = new LinkedList<>(); + for (String sourcePath : sourcePaths) { + if (new org.eclipse.core.runtime.Path(sourcePath).isAbsolute()) { + throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "The source path must be a relative path to the workspace.")); } + sourceList.add(workspaceLinkFolder.getFolder(sourcePath).getFullPath()); } + return sourceList; + } - if (forceUpdateLibPath) { - PreferenceManager preferencesManager = JavaLanguageServerPlugin.getPreferencesManager(); - if (preferencesManager != null && preferencesManager.getPreferences() != null) { - IJavaProject javaProject = JavaCore.create(invisibleProject); - UpdateClasspathJob.getInstance().updateClasspath(javaProject, preferencesManager.getPreferences().getReferencedLibraries()); - } + public static List getExcludingPath(IJavaProject javaProject, IPath rootPath, IFolder workspaceLinkFolder) throws CoreException { + if (rootPath == null) { + rootPath = ProjectUtils.findBelongedWorkspaceRoot(workspaceLinkFolder.getLocation()); } - return true; + if (rootPath == null) { + throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "Failed to find the belonging root of the linked folder: " + workspaceLinkFolder.toString())); + } + + final IPath root = rootPath; + IPath libFolder = new Path(InvisibleProjectBuildSupport.LIB_FOLDER); + List subProjects = ProjectUtils.getVisibleProjects(rootPath); + List excludingPaths = subProjects.stream().map(project -> { + IPath relativePath = project.getLocation().makeRelativeTo(root); + return workspaceLinkFolder.getFolder(relativePath).getFullPath(); + }).collect(Collectors.toList()); + excludingPaths.add(libFolder); + + return excludingPaths; } private static boolean isPartOfMatureProject(IPath sourcePath) { diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectPreferenceChangeListener.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectPreferenceChangeListener.java index 587d725e3a..5870fc6380 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectPreferenceChangeListener.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectPreferenceChangeListener.java @@ -13,11 +13,15 @@ package org.eclipse.jdt.ls.core.internal.managers; +import java.util.List; import java.util.Objects; +import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jdt.ls.core.internal.ProjectUtils; @@ -30,8 +34,8 @@ public class InvisibleProjectPreferenceChangeListener implements IPreferencesCha @Override public void preferencesChange(Preferences oldPreferences, Preferences newPreferences) { - boolean projectOutputPathChanged = !Objects.equals(oldPreferences.getInvisibleProjectOutputPath(), newPreferences.getInvisibleProjectOutputPath()); - if (projectOutputPathChanged) { + if (!Objects.equals(oldPreferences.getInvisibleProjectOutputPath(), newPreferences.getInvisibleProjectOutputPath()) || + !Objects.equals(oldPreferences.getInvisibleProjectSourcePaths(), newPreferences.getInvisibleProjectSourcePaths())) { for (IJavaProject javaProject : ProjectUtils.getJavaProjects()) { IProject project = javaProject.getProject(); if (ProjectUtils.isVisibleProject(project)) { @@ -40,13 +44,21 @@ public void preferencesChange(Preferences oldPreferences, Preferences newPrefere if (project.equals(ProjectsManager.getDefaultProject())) { continue; } + try { - InvisibleProjectImporter.setInvisibleProjectOutputPath(javaProject, newPreferences.getInvisibleProjectOutputPath(), true /*isUpdate*/, new NullProgressMonitor()); + IFolder workspaceLinkFolder = javaProject.getProject().getFolder(ProjectUtils.WORKSPACE_LINK); + if (!workspaceLinkFolder.exists()) { + continue; + } + List sourcePaths = InvisibleProjectImporter.getSourcePaths(newPreferences.getInvisibleProjectSourcePaths(), workspaceLinkFolder); + List excludingPaths = InvisibleProjectImporter.getExcludingPath(javaProject, null, workspaceLinkFolder); + IPath outputPath = InvisibleProjectImporter.getOutputPath(javaProject, newPreferences.getInvisibleProjectOutputPath(), true /*isUpdate*/); + IClasspathEntry[] classpathEntries = InvisibleProjectImporter.resolveClassPathEntries(javaProject, sourcePaths, excludingPaths, outputPath); + javaProject.setRawClasspath(classpathEntries, outputPath, new NullProgressMonitor()); } catch (CoreException e) { JavaLanguageServerPlugin.getProjectsManager().getConnection().showMessage(new MessageParams(MessageType.Error, e.getMessage())); } } } } - } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java index 6ef8bc97fe..2fd6e8ebd3 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java @@ -225,6 +225,11 @@ public class Preferences { */ public static final String JAVA_PROJECT_OUTPUT_PATH_KEY = "java.project.outputPath"; + /** + * Preference key to specify the source paths of the invisible project + */ + public static final String JAVA_PROJECT_SOURCE_PATHS_KEY = "java.project.sourcePaths"; + /** * Preference key for project build/configuration update settings. */ @@ -488,6 +493,7 @@ public class Preferences { private List javaImportExclusions = new LinkedList<>(); private ReferencedLibraries referencedLibraries; private String invisibleProjectOutputPath; + private List invisibleProjectSourcePaths; private String javaHome; private List importOrder; private List filteredTypes; @@ -829,6 +835,9 @@ public static Preferences createFrom(Map configuration) { String invisibleProjectOutputPath = getString(configuration, JAVA_PROJECT_OUTPUT_PATH_KEY, ""); prefs.setInvisibleProjectOutputPath(invisibleProjectOutputPath); + List invisibleProjectSourcePaths = getList(configuration, JAVA_PROJECT_SOURCE_PATHS_KEY, null); + prefs.setInvisibleProjectSourcePaths(invisibleProjectSourcePaths); + List javaCompletionFavoriteMembers = getList(configuration, JAVA_COMPLETION_FAVORITE_MEMBERS_KEY, JAVA_COMPLETION_FAVORITE_MEMBERS_DEFAULT); prefs.setJavaCompletionFavoriteMembers(javaCompletionFavoriteMembers); @@ -1603,6 +1612,14 @@ public void setInvisibleProjectOutputPath(String invisibleProjectOutputPath) { this.invisibleProjectOutputPath = invisibleProjectOutputPath; } + public List getInvisibleProjectSourcePaths() { + return invisibleProjectSourcePaths; + } + + public void setInvisibleProjectSourcePaths(List invisibleProjectSourcePaths) { + this.invisibleProjectSourcePaths = invisibleProjectSourcePaths; + } + public Preferences setIncludeDecompiledSources(boolean includeDecompiledSources) { this.includeDecompiledSources = includeDecompiledSources; return this; @@ -1639,5 +1656,4 @@ public void updateTabSizeInsertSpaces(Hashtable options) { } options.put(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, insertSpaces ? JavaCore.SPACE : JavaCore.TAB); } - } diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/AbstractProjectsManagerBasedTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/AbstractProjectsManagerBasedTest.java index ae05e97449..57afb26d26 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/AbstractProjectsManagerBasedTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/AbstractProjectsManagerBasedTest.java @@ -132,9 +132,7 @@ public void initProjectManager() throws Exception { logListener = new SimpleLogListener(); Platform.addLogListener(logListener); preferences = new Preferences(); - preferences.setRootPaths(Collections.singleton(new Path(getWorkingProjectDirectory().getAbsolutePath()))); - preferences.setCodeGenerationTemplateGenerateComments(true); - preferences.setMavenDownloadSources(true); + initPreferences(preferences); if (preferenceManager == null) { preferenceManager = mock(StandardPreferenceManager.class); } @@ -160,6 +158,12 @@ public IBuffer createBuffer(ICompilationUnit workingCopy) { }); } + protected void initPreferences(Preferences preferences) throws IOException { + preferences.setRootPaths(Collections.singleton(new Path(getWorkingProjectDirectory().getAbsolutePath()))); + preferences.setCodeGenerationTemplateGenerateComments(true); + preferences.setMavenDownloadSources(true); + } + protected ClientPreferences initPreferenceManager(boolean supportClassFileContents) { StandardPreferenceManager.initialize(); Hashtable javaCoreOptions = JavaCore.getOptions(); diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectBuildSupportTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectBuildSupportTest.java index e0ee9aacb8..ec33e4787a 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectBuildSupportTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectBuildSupportTest.java @@ -34,7 +34,6 @@ import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IPath; -import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.IJobChangeListener; import org.eclipse.core.runtime.jobs.Job; @@ -510,16 +509,4 @@ public void testDynamicSourceLookups() throws Exception { String javadoc = hover.getContents().getLeft().get(1).getLeft(); assertTrue("Unexpected Javadoc:" + javadoc, javadoc.contains("The class that manages converting HTML to Markdown")); } - - public void testUpdateOutputPath() throws Exception { - preferenceManager.getPreferences().setInvisibleProjectOutputPath(""); - IProject project = copyAndImportFolder("singlefile/simple", "src/App.java"); - IJavaProject javaProject = JavaCore.create(project); - assertEquals(String.join("/", "", javaProject.getElementName(), "bin"), javaProject.getOutputLocation().toString()); - - InvisibleProjectImporter.setInvisibleProjectOutputPath(javaProject, "bin", true /*isUpdate*/, new NullProgressMonitor()); - waitForBackgroundJobs(); - - assertEquals(String.join("/", "", javaProject.getElementName(), ProjectUtils.WORKSPACE_LINK, "bin"), javaProject.getOutputLocation().toString()); - } } diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectImporterTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectImporterTest.java index 630038c514..747259944f 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectImporterTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectImporterTest.java @@ -19,8 +19,12 @@ import static org.mockito.Mockito.when; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; @@ -222,10 +226,11 @@ public void testSpecifyingOutputPath() throws Exception { @Test public void testSpecifyingOutputPathInsideSourcePath() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectOutputPath("output"); IProject invisibleProject = copyAndImportFolder("singlefile/java14", "foo/bar/Foo.java"); waitForBackgroundJobs(); IJavaProject javaProject = JavaCore.create(invisibleProject); - InvisibleProjectImporter.setInvisibleProjectOutputPath(javaProject, "output", true /*isUpdate*/, null); boolean isOutputExcluded = false; for (IClasspathEntry entry : javaProject.getRawClasspath()) { if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) { @@ -243,26 +248,147 @@ public void testSpecifyingOutputPathInsideSourcePath() throws Exception { @Test(expected = CoreException.class) public void testSpecifyingOutputPathEqualToSourcePath() throws Exception { - IProject invisibleProject = copyAndImportFolder("singlefile/simple", "src/App.java"); + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectOutputPath("src"); + copyAndImportFolder("singlefile/simple", "src/App.java"); waitForBackgroundJobs(); - IJavaProject javaProject = JavaCore.create(invisibleProject); - InvisibleProjectImporter.setInvisibleProjectOutputPath(javaProject, "src", true /*isUpdate*/, null); } @Test(expected = CoreException.class) - public void testSpecifyingOutputPathToUnEmptyFolder() throws Exception { - IProject invisibleProject = copyAndImportFolder("singlefile/simple", "src/App.java"); + public void testSpecifyingAbsoluteOutputPath() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectOutputPath(new File("projects").getAbsolutePath()); + copyAndImportFolder("singlefile/simple", "src/App.java"); waitForBackgroundJobs(); - IJavaProject javaProject = JavaCore.create(invisibleProject); - InvisibleProjectImporter.setInvisibleProjectOutputPath(javaProject, "lib", true /*isUpdate*/, null); } @Test public void testSpecifyingEmptyOutputPath() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectOutputPath(""); IProject invisibleProject = copyAndImportFolder("singlefile/simple", "src/App.java"); waitForBackgroundJobs(); IJavaProject javaProject = JavaCore.create(invisibleProject); - InvisibleProjectImporter.setInvisibleProjectOutputPath(javaProject, "", true /*isUpdate*/, null); assertEquals(String.join("/", "", javaProject.getElementName(), "bin"), javaProject.getOutputLocation().toString()); } + + @Test + public void testSpecifyingSourcePaths() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectSourcePaths(Arrays.asList("foo", "bar")); + IProject invisibleProject = copyAndImportFolder("singlefile/java14", "foo/bar/Foo.java"); + waitForBackgroundJobs(); + IJavaProject javaProject = JavaCore.create(invisibleProject); + IFolder linkFolder = invisibleProject.getFolder(ProjectUtils.WORKSPACE_LINK); + + List sourcePaths = new ArrayList<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourcePaths.add(entry.getPath().makeRelativeTo(linkFolder.getFullPath()).toString()); + } + } + assertEquals(2, sourcePaths.size()); + assertTrue(sourcePaths.contains("foo")); + assertTrue(sourcePaths.contains("bar")); + } + + @Test + public void testSpecifyingEmptySourcePaths() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectSourcePaths(Collections.emptyList()); + IProject invisibleProject = copyAndImportFolder("singlefile/java14", "foo/bar/Foo.java"); + waitForBackgroundJobs(); + IJavaProject javaProject = JavaCore.create(invisibleProject); + IFolder linkFolder = invisibleProject.getFolder(ProjectUtils.WORKSPACE_LINK); + + List sourcePaths = new ArrayList<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourcePaths.add(entry.getPath().makeRelativeTo(linkFolder.getFullPath()).toString()); + } + } + assertEquals(0, sourcePaths.size()); + } + + @Test + public void testSpecifyingNestedSourcePaths() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectSourcePaths(Arrays.asList("foo", "foo/bar")); + IProject invisibleProject = copyAndImportFolder("singlefile/java14", "foo/bar/Foo.java"); + waitForBackgroundJobs(); + IJavaProject javaProject = JavaCore.create(invisibleProject); + IFolder linkFolder = invisibleProject.getFolder(ProjectUtils.WORKSPACE_LINK); + + List sourcePaths = new ArrayList<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourcePaths.add(entry.getPath().makeRelativeTo(linkFolder.getFullPath()).toString()); + } + } + assertEquals(2, sourcePaths.size()); + assertTrue(sourcePaths.contains("foo")); + assertTrue(sourcePaths.contains("foo/bar")); + } + + @Test + public void testSpecifyingDuplicatedSourcePaths() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectSourcePaths(Arrays.asList("foo", "foo")); + IProject invisibleProject = copyAndImportFolder("singlefile/java14", "foo/bar/Foo.java"); + waitForBackgroundJobs(); + IJavaProject javaProject = JavaCore.create(invisibleProject); + IFolder linkFolder = invisibleProject.getFolder(ProjectUtils.WORKSPACE_LINK); + + List sourcePaths = new ArrayList<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourcePaths.add(entry.getPath().makeRelativeTo(linkFolder.getFullPath()).toString()); + } + } + assertEquals(1, sourcePaths.size()); + assertTrue(sourcePaths.contains("foo")); + } + + @Test + public void testSpecifyingRootAsSourcePaths() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectSourcePaths(Arrays.asList("")); + IProject invisibleProject = copyAndImportFolder("singlefile/java14", "foo/bar/Foo.java"); + waitForBackgroundJobs(); + IJavaProject javaProject = JavaCore.create(invisibleProject); + IFolder linkFolder = invisibleProject.getFolder(ProjectUtils.WORKSPACE_LINK); + + List sourcePaths = new ArrayList<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourcePaths.add(entry.getPath().makeRelativeTo(linkFolder.getFullPath()).toString()); + } + } + assertEquals(1, sourcePaths.size()); + assertTrue(sourcePaths.contains("")); + } + + @Test(expected = CoreException.class) + public void testSpecifyingAbsoluteSourcePath() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectSourcePaths(Arrays.asList(new File("projects").getAbsolutePath())); + copyAndImportFolder("singlefile/simple", "src/App.java"); + waitForBackgroundJobs(); + } + + @Test + public void testSpecifyingSourcePathsContainingOutputPath() throws Exception { + Preferences preferences = preferenceManager.getPreferences(); + preferences.setInvisibleProjectSourcePaths(Arrays.asList("")); + preferences.setInvisibleProjectOutputPath("bin"); + IProject invisibleProject = copyAndImportFolder("singlefile/java14", "foo/bar/Foo.java"); + waitForBackgroundJobs(); + IJavaProject javaProject = JavaCore.create(invisibleProject); + + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + assertEquals("bin/", entry.getExclusionPatterns()[0].toString()); + } + } + } } diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectPreferenceChangeListenerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectPreferenceChangeListenerTest.java new file mode 100644 index 0000000000..bf649367b5 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/InvisibleProjectPreferenceChangeListenerTest.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2021 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.jdt.ls.core.internal.managers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; +import org.eclipse.jdt.ls.core.internal.JavaClientConnection.JavaLanguageClient; +import org.eclipse.jdt.ls.core.internal.preferences.Preferences; +import org.eclipse.lsp4j.MessageParams; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * InvisibleProjectPreferenceChangeListenerTest + */ +@RunWith(MockitoJUnitRunner.class) +public class InvisibleProjectPreferenceChangeListenerTest extends AbstractInvisibleProjectBasedTest { + + @Test + public void testUpdateOutputPath() throws Exception { + preferenceManager.getPreferences().setInvisibleProjectOutputPath(""); + IProject project = copyAndImportFolder("singlefile/simple", "src/App.java"); + IJavaProject javaProject = JavaCore.create(project); + assertEquals(String.join("/", "", javaProject.getElementName(), "bin"), javaProject.getOutputLocation().toString()); + + Preferences newPreferences = new Preferences(); + initPreferences(newPreferences); + newPreferences.setInvisibleProjectOutputPath("bin"); + InvisibleProjectPreferenceChangeListener listener = new InvisibleProjectPreferenceChangeListener(); + listener.preferencesChange(preferenceManager.getPreferences(), newPreferences); + waitForBackgroundJobs(); + + assertEquals(String.join("/", "", javaProject.getElementName(), ProjectUtils.WORKSPACE_LINK, "bin"), javaProject.getOutputLocation().toString()); + } + + @Test + public void testUpdateOutputPathToUnEmptyFolder() throws Exception { + copyAndImportFolder("singlefile/simple", "src/App.java"); + waitForBackgroundJobs(); + + Preferences newPreferences = new Preferences(); + initPreferences(newPreferences); + newPreferences.setInvisibleProjectOutputPath("lib"); + + JavaLanguageClient client = mock(JavaLanguageClient.class); + ProjectsManager pm = JavaLanguageServerPlugin.getProjectsManager(); + pm.setConnection(client); + doNothing().when(client).showMessage(any(MessageParams.class)); + InvisibleProjectPreferenceChangeListener listener = new InvisibleProjectPreferenceChangeListener(); + listener.preferencesChange(preferenceManager.getPreferences(), newPreferences); + waitForBackgroundJobs(); + + verify(client, times(1)).showMessage(any(MessageParams.class)); + } + + @Test + public void testUpdateSourcePaths() throws Exception { + preferenceManager.getPreferences().setInvisibleProjectOutputPath(""); + IProject project = copyAndImportFolder("singlefile/simple", "src/App.java"); + IJavaProject javaProject = JavaCore.create(project); + IFolder linkFolder = project.getFolder(ProjectUtils.WORKSPACE_LINK); + + List sourcePaths = new ArrayList<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourcePaths.add(entry.getPath().makeRelativeTo(linkFolder.getFullPath()).toString()); + } + } + assertEquals(1, sourcePaths.size()); + assertTrue(sourcePaths.contains("src")); + + Preferences newPreferences = new Preferences(); + initPreferences(newPreferences); + newPreferences.setInvisibleProjectSourcePaths(Arrays.asList("src", "src2")); + InvisibleProjectPreferenceChangeListener listener = new InvisibleProjectPreferenceChangeListener(); + listener.preferencesChange(preferenceManager.getPreferences(), newPreferences); + + waitForBackgroundJobs(); + + sourcePaths.clear(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourcePaths.add(entry.getPath().makeRelativeTo(linkFolder.getFullPath()).toString()); + } + } + + assertEquals(2, sourcePaths.size()); + assertTrue(sourcePaths.contains("src")); + assertTrue(sourcePaths.contains("src2")); + } +} \ No newline at end of file