Make jdt.ls extension more reliable detecting deleted projects
This commit is contained in:
@@ -12,6 +12,5 @@ Require-Bundle: org.springframework.tooling.jdt.ls.commons,
|
||||
org.apache.commons.lang3,
|
||||
org.junit,
|
||||
org.apache.commons.io,
|
||||
org.eclipse.swt,
|
||||
com.google.guava;bundle-version="21.0.0"
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.tooling.jdt.ls.commons.Logger.TestLogger;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.Classpath;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.Classpath.CPE;
|
||||
import org.springframework.tooling.jdt.ls.commons.test.ClasspathListenerHandlerTest.MockClientCommandExecutor;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.ClientCommandExecutor;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.ReusableClasspathListenerHandler;
|
||||
import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition;
|
||||
@@ -53,13 +54,15 @@ import org.springsource.ide.eclipse.commons.frameworks.test.util.Asserter;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import junit.framework.Assert;
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
public class ClasspathListenerHandlerTest {
|
||||
|
||||
private TestLogger logger = new TestLogger();
|
||||
private MockClientCommandExecutor commandExecutor = new MockClientCommandExecutor();
|
||||
private MockClasspathCache classpaths = new MockClasspathCache();
|
||||
private ReusableClasspathListenerHandler service = new ReusableClasspathListenerHandler(logger, classpaths);
|
||||
private ReusableClasspathListenerHandler service = new ReusableClasspathListenerHandler(logger, commandExecutor);
|
||||
|
||||
@Test public void classpathIsSentForExistingProject() throws Exception {
|
||||
String projectName = "classpath-test-simple-java-project";
|
||||
@@ -75,7 +78,7 @@ public class ClasspathListenerHandlerTest {
|
||||
}
|
||||
|
||||
|
||||
@Ignore //TODO: fails randomly for unknown reason.
|
||||
//@Ignore //TODO: fails randomly for unknown reason.
|
||||
@Test public void classpathIsSentForNewProject_and_removedForDeletedProject() throws Exception {
|
||||
service.addClasspathListener(classpaths.commandId);
|
||||
|
||||
@@ -95,7 +98,39 @@ public class ClasspathListenerHandlerTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Ignore //TODO: fails randomly for unknown reason.
|
||||
@Test public void classpathIsRemovedWhenProjectDeletedFromFileSystem_twoSubscribers() throws Exception {
|
||||
MockClasspathCache secondListener = new MockClasspathCache();
|
||||
try {
|
||||
String projectName = "classpath-test-simple-java-project";
|
||||
IProject project = createTestProject(projectName);
|
||||
File loc = project.getLocation().toFile();
|
||||
|
||||
service.addClasspathListener(classpaths.commandId);
|
||||
ACondition.waitFor("Project with classpath to appear", Duration.ofSeconds(5), () -> {
|
||||
Classpath cp = classpaths.getFor(loc).classpath;
|
||||
assertTrue(cp.getEntries().stream().filter(cpe -> Classpath.isSource(cpe)).count()==1); //has 1 source entry
|
||||
assertClasspath(cp, cp.getEntries().stream().filter(cpe -> Classpath.isBinary(cpe) && cpe.isSystem()).count()>=1); //has some system libraries
|
||||
});
|
||||
|
||||
FileUtils.deleteQuietly(loc);
|
||||
safe(() -> project.refreshLocal(IResource.DEPTH_INFINITE, null));
|
||||
|
||||
ACondition.waitFor("Project to disapear", Duration.ofSeconds(5), () -> {
|
||||
{
|
||||
Info cp = classpaths.getFor(loc);
|
||||
assertNull(cp);
|
||||
}
|
||||
{
|
||||
Info cp = secondListener.getFor(loc);
|
||||
assertNull(cp);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
secondListener.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
//@Ignore //TODO: fails randomly for unknown reason.
|
||||
@Test public void classpathIsRemovedWhenProjectDeletedFromFileSystem() throws Exception {
|
||||
String projectName = "classpath-test-simple-java-project";
|
||||
IProject project = createTestProject(projectName);
|
||||
@@ -117,7 +152,7 @@ public class ClasspathListenerHandlerTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Ignore //TODO: why is this failing in CI builds but passing locally?
|
||||
//@Ignore //TODO: why is this failing in CI builds but passing locally?
|
||||
@Test public void sourceJar() throws Exception {
|
||||
String projectName = "maven-with-jar-dependency";
|
||||
IProject project = createTestProject(projectName);
|
||||
@@ -151,12 +186,39 @@ public class ClasspathListenerHandlerTest {
|
||||
}
|
||||
}
|
||||
|
||||
public class MockClientCommandExecutor implements ClientCommandExecutor {
|
||||
|
||||
private Map<String,ClientCommandExecutor> handlers = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public synchronized Object executeClientCommand(String id, Object... params) throws Exception {
|
||||
ClientCommandExecutor handler = handlers.get(id);
|
||||
if (handler!=null) {
|
||||
return handler.executeClientCommand(id, params);
|
||||
}
|
||||
throw new IllegalStateException("No handler for "+id);
|
||||
}
|
||||
|
||||
public void addHandler(String id, ClientCommandExecutor handler) {
|
||||
Assert.assertFalse(handlers.containsKey(id));
|
||||
handlers.put(id, handler);
|
||||
}
|
||||
|
||||
public void removeHandler(String commandId) {
|
||||
handlers.remove(commandId);
|
||||
}
|
||||
}
|
||||
|
||||
public class MockClasspathCache implements ClientCommandExecutor {
|
||||
|
||||
String commandId = RandomStringUtils.randomAlphabetic(8);
|
||||
|
||||
Map<File, Info> classpaths = new HashMap<>();
|
||||
|
||||
{
|
||||
commandExecutor.addHandler(commandId, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Object executeClientCommand(String id, Object... _params) throws Exception {
|
||||
if (id.equals(commandId)) {
|
||||
@@ -183,6 +245,7 @@ public class ClasspathListenerHandlerTest {
|
||||
|
||||
public void dispose() throws Exception {
|
||||
service.removeClasspathListener(commandId);
|
||||
commandExecutor.removeHandler(commandId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import java.util.Map;
|
||||
|
||||
import org.eclipse.core.runtime.jobs.IJobManager;
|
||||
import org.eclipse.core.runtime.jobs.Job;
|
||||
import org.eclipse.swt.widgets.Display;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
@@ -223,29 +222,29 @@ public abstract class ACondition {
|
||||
* but such calls have no effect.
|
||||
*/
|
||||
public static void waitForDisplay() {
|
||||
if (inUIThread()) {
|
||||
try {
|
||||
while (Display.getDefault().readAndDispatch()) {
|
||||
// do nothing
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// in e 44 this is throwing exceptions... a lot. Log them in case they contain
|
||||
// some valuable hints... but move along. These errors happen because some component
|
||||
// probably unrelated to our tests misbehaved. I suspect GTK3 may be causing
|
||||
// NPEs and other errors in the Eclipse UI code, for example. These errors seem
|
||||
// to propagate out of the 'readAndDispatch' call on e44.
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
// if (inUIThread()) {
|
||||
// try {
|
||||
// while (Display.getDefault().readAndDispatch()) {
|
||||
// // do nothing
|
||||
// }
|
||||
// } catch (Throwable e) {
|
||||
// // in e 44 this is throwing exceptions... a lot. Log them in case they contain
|
||||
// // some valuable hints... but move along. These errors happen because some component
|
||||
// // probably unrelated to our tests misbehaved. I suspect GTK3 may be causing
|
||||
// // NPEs and other errors in the Eclipse UI code, for example. These errors seem
|
||||
// // to propagate out of the 'readAndDispatch' call on e44.
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
public static boolean inUIThread() {
|
||||
try {
|
||||
return Display.getDefault().getThread() == Thread.currentThread();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// try {
|
||||
// return Display.getDefault().getThread() == Thread.currentThread();
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
public static StringBuffer getStackDumps() {
|
||||
|
||||
@@ -87,44 +87,11 @@ public class ClasspathListenerManager {
|
||||
private MyListener myListener;
|
||||
private final Logger logger;
|
||||
|
||||
/**
|
||||
* @param initialEvent If true, events are fired immediately on all existing java
|
||||
* projects, treating the connection of the listener itself as a change event.
|
||||
* This allows clients to become aware of all classpaths from the start and
|
||||
* continually monitor them for changes from that point onward.
|
||||
*/
|
||||
public ClasspathListenerManager(Logger logger, ClasspathListener listener, boolean initialEvent, Supplier<Comparator<IProject>> projectSorterFactory) {
|
||||
public ClasspathListenerManager(Logger logger, ClasspathListener listener) {
|
||||
this.logger = logger;
|
||||
logger.log("Setting up ClasspathListenerManager");
|
||||
this.listener = listener;
|
||||
JavaCore.addElementChangedListener(myListener=new MyListener(), ElementChangedEvent.POST_CHANGE);
|
||||
if (initialEvent) {
|
||||
logger.log("Sending initial event for all projects ...");
|
||||
|
||||
IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
|
||||
if (projectSorterFactory != null) {
|
||||
Arrays.sort(projects, projectSorterFactory.get());
|
||||
}
|
||||
|
||||
for (IProject p : projects) {
|
||||
logger.log("project "+p.getName() +" ..." );
|
||||
try {
|
||||
if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) {
|
||||
IJavaProject jp = JavaCore.create(p);
|
||||
listener.classpathChanged(jp);
|
||||
} else {
|
||||
logger.log("project "+p.getName() +" SKIPPED" );
|
||||
}
|
||||
} catch (CoreException e) {
|
||||
logger.log(e);
|
||||
}
|
||||
}
|
||||
logger.log("Sending initial event for all projects DONE");
|
||||
}
|
||||
}
|
||||
|
||||
public ClasspathListenerManager(Logger logger, ClasspathListener listener) {
|
||||
this(logger, listener, false, null);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
|
||||
@@ -12,17 +12,25 @@ package org.springframework.tooling.jdt.ls.commons.classpath;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.core.resources.IProject;
|
||||
import org.eclipse.core.resources.ResourcesPlugin;
|
||||
import org.eclipse.core.runtime.CoreException;
|
||||
import org.eclipse.core.runtime.IProgressMonitor;
|
||||
import org.eclipse.core.runtime.IStatus;
|
||||
import org.eclipse.core.runtime.Status;
|
||||
import org.eclipse.core.runtime.jobs.Job;
|
||||
import org.eclipse.jdt.core.IJavaProject;
|
||||
import org.eclipse.jdt.core.JavaCore;
|
||||
import org.springframework.tooling.jdt.ls.commons.Logger;
|
||||
import org.springframework.tooling.jdt.ls.commons.classpath.ClasspathListenerManager.ClasspathListener;
|
||||
|
||||
@@ -74,7 +82,7 @@ public class ReusableClasspathListenerHandler {
|
||||
URI loc = getProjectLocation(jp);
|
||||
if (loc!=null) {
|
||||
File f = new File(loc);
|
||||
return f.isDirectory();
|
||||
return f.isDirectory() && jp.getProject().hasNature(JavaCore.NATURE_ID);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//Something bogus about this project... so just pretend it doesn't exist.
|
||||
@@ -86,23 +94,51 @@ public class ReusableClasspathListenerHandler {
|
||||
|
||||
class Subscriptions {
|
||||
|
||||
private Map<String, ClasspathListenerManager> subscribers = null;
|
||||
private Set<String> subscribers = null;
|
||||
private ClasspathListenerManager classpathListener = null;
|
||||
|
||||
public synchronized void subscribe(String callbackCommandId) {
|
||||
logger.log("subscribing to classpath changes: " + callbackCommandId);
|
||||
if (subscribers==null) {
|
||||
subscribers = new HashMap<>(1);
|
||||
//First subscriber
|
||||
subscribers = new HashSet<>();
|
||||
classpathListener = new ClasspathListenerManager(logger, new ClasspathListener() {
|
||||
@Override
|
||||
public void classpathChanged(IJavaProject jp) {
|
||||
sendNotification(jp, subscribers);
|
||||
}
|
||||
});
|
||||
}
|
||||
subscribers.computeIfAbsent(callbackCommandId, (cid) -> new ClasspathListenerManager(logger, new ClasspathListener() {
|
||||
@Override
|
||||
public void classpathChanged(IJavaProject jp) {
|
||||
sendNotification(callbackCommandId, jp);
|
||||
subscribers.add(callbackCommandId);
|
||||
logger.log("subsribers = " + subscribers);
|
||||
sendInitialEvents(callbackCommandId);
|
||||
}
|
||||
|
||||
private void sendInitialEvents(String callbackCommandId) {
|
||||
logger.log("Sending initial event for all projects ...");
|
||||
|
||||
IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
|
||||
if (projectSorterFactory != null) {
|
||||
Arrays.sort(projects, projectSorterFactory.get());
|
||||
}
|
||||
|
||||
for (IProject p : projects) {
|
||||
logger.log("project "+p.getName() +" ..." );
|
||||
try {
|
||||
if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) {
|
||||
IJavaProject jp = JavaCore.create(p);
|
||||
sendNotification(jp, Collections.singleton(callbackCommandId));
|
||||
} else {
|
||||
logger.log("project "+p.getName() +" SKIPPED" );
|
||||
}
|
||||
} catch (CoreException e) {
|
||||
logger.log(e);
|
||||
}
|
||||
}, true, projectSorterFactory));
|
||||
logger.log("subsribers = " + subscribers.keySet());
|
||||
}
|
||||
logger.log("Sending initial event for all projects DONE");
|
||||
}
|
||||
|
||||
private void sendNotification(String callbackCommandId, IJavaProject jp) {
|
||||
private void sendNotification(IJavaProject jp, Collection<String> callbackIds) {
|
||||
//TODO: make one Job to accumulate all requested notification and work more efficiently by batching
|
||||
// and avoiding multiple executions of duplicated requests.
|
||||
Job job = new Job("SendClasspath notification") {
|
||||
@@ -116,7 +152,7 @@ public class ReusableClasspathListenerHandler {
|
||||
logger.log("Could not send event for project because no project location: "+jp.getElementName());
|
||||
} else {
|
||||
boolean exsits = projectExists(jp);
|
||||
boolean open = true; // WARNING: calling is jp.isOpen is unreliable and subject to race condition. After a POST_CHAGE project open event
|
||||
boolean open = true; // WARNING: calling jp.isOpen is unreliable and subject to race condition. After a POST_CHAGE project open event
|
||||
// this should be true but it typically is not unless you wait for some time. No idea how you would know
|
||||
// how long you should wait (200ms is not enough, and that seems pretty long). Isn't it kind of the point
|
||||
// for a 'POST_CHANGE' event to come **after** model has already changed? I guess not in Eclipse.
|
||||
@@ -128,7 +164,7 @@ public class ReusableClasspathListenerHandler {
|
||||
|
||||
Classpath classpath = Classpath.EMPTY;
|
||||
if (deleted) {
|
||||
projectLocations.remove(projectName);
|
||||
// projectLocations.remove(projectName);
|
||||
} else {
|
||||
projectLocations.put(projectName, projectLoc);
|
||||
try {
|
||||
@@ -137,13 +173,15 @@ public class ReusableClasspathListenerHandler {
|
||||
logger.log(e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
logger.log("executing callback "+callbackCommandId+" "+projectName+" "+deleted+" "+ classpath.getEntries().size());
|
||||
Object r = conn.executeClientCommand(callbackCommandId, projectLoc.toString(), projectName, deleted, classpath);
|
||||
logger.log("executing callback "+callbackCommandId+" SUCCESS ["+r+"]");
|
||||
} catch (Exception e) {
|
||||
logger.log("executing callback "+callbackCommandId+" FAILED");
|
||||
logger.log(e);
|
||||
for (String callbackCommandId : callbackIds) {
|
||||
try {
|
||||
logger.log("executing callback "+callbackCommandId+" "+projectName+" "+deleted+" "+ classpath.getEntries().size());
|
||||
Object r = conn.executeClientCommand(callbackCommandId, projectLoc.toString(), projectName, deleted, classpath);
|
||||
logger.log("executing callback "+callbackCommandId+" SUCCESS ["+r+"]");
|
||||
} catch (Exception e) {
|
||||
logger.log("executing callback "+callbackCommandId+" FAILED");
|
||||
logger.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -159,15 +197,16 @@ public class ReusableClasspathListenerHandler {
|
||||
public synchronized void unsubscribe(String callbackCommandId) {
|
||||
logger.log("unsubscribing from classpath changes: " + callbackCommandId);
|
||||
if (subscribers != null) {
|
||||
ClasspathListenerManager mgr = subscribers.remove(callbackCommandId);
|
||||
if (mgr!=null) {
|
||||
mgr.dispose();
|
||||
}
|
||||
subscribers.remove(callbackCommandId);
|
||||
if (subscribers.isEmpty()) {
|
||||
subscribers = null;
|
||||
if (classpathListener!=null) {
|
||||
classpathListener.dispose();
|
||||
classpathListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.log("subsribers = " + (subscribers == null ? "null" : subscribers.keySet()));
|
||||
logger.log("subsribers = " + subscribers);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<?pde version="3.8"?><target name="Java Language Server Target Definition" sequenceNumber="111">
|
||||
<locations>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="org.mockito.mockito-all" version="1.9.5"/>
|
||||
<repository location="http://download.eclipse.org/scout/releases/4.0/testing"/>
|
||||
</location>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="com.google.gson" version="2.7.0.v20170129-0911"/>
|
||||
<unit id="com.google.gson.source" version="2.7.0.v20170129-0911"/>
|
||||
<unit id="com.ibm.icu.base" version="58.2.0.v20170418-1837"/>
|
||||
<unit id="org.apache.commons.io" version="2.2.0.v201405211200"/>
|
||||
<unit id="org.apache.commons.lang3" version="3.1.0.v201403281430"/>
|
||||
<unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
|
||||
<repository location="http://download.eclipse.org/tools/orbit/R-builds/R20170516192513/repository"/>
|
||||
</location>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="org.eclipse.buildship.feature.group" version="2.2.0.v20171211-1404"/>
|
||||
<repository location="http://download.eclipse.org/buildship/updates/e47/releases/2.x/"/>
|
||||
</location>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="org.eclipse.m2e.feature.feature.group" version="1.9.0.20180313-2237"/>
|
||||
<repository location="http://download.eclipse.org/technology/m2e/milestones/1.9/1.9.0.20180313-2237/"/>
|
||||
</location>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="org.eclipse.equinox.core.feature.feature.group" version="1.5.0.v20180419-0833"/>
|
||||
<unit id="org.eclipse.equinox.core.sdk.feature.group" version="3.14.0.v20180412-1130"/>
|
||||
<unit id="org.eclipse.equinox.executable.feature.group" version="3.8.0.v20180326-1331"/>
|
||||
<unit id="org.eclipse.equinox.p2.core.feature.source.feature.group" version="1.5.0.v20180413-0846"/>
|
||||
<unit id="org.eclipse.equinox.sdk.feature.group" version="3.14.0.v20180419-0833"/>
|
||||
<unit id="org.eclipse.jdt.source.feature.group" version="3.14.0.v20180423-1043"/>
|
||||
<unit id="org.eclipse.sdk.feature.group" version="4.8.0.v20180423-1043"/>
|
||||
<repository location="http://download.eclipse.org/eclipse/updates/4.8-I-builds/I20180423-0655/"/>
|
||||
</location>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="org.eclipse.lsp4j.sdk.feature.group" version="0.4.0.v20180301-0956"/>
|
||||
<repository location="http://download.eclipse.org/lsp4j/updates/milestones/"/>
|
||||
</location>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="org.eclipse.xtend.sdk.feature.group" version="2.13.0.v20171020-0920"/>
|
||||
<unit id="org.eclipse.xtext.sdk.feature.group" version="2.13.0.v20171020-0920"/>
|
||||
<repository location="http://download.eclipse.org/releases/photon/"/>
|
||||
</location>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="org.jboss.tools.maven.apt.feature.feature.group" version="1.5.0.201804060341"/>
|
||||
<repository location="http://download.jboss.org/jbosstools/updates/m2e-extensions/m2e-apt/1.5.0-2018-04-06_03-47-50-H10"/>
|
||||
</location>
|
||||
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="slicer" includeSource="true" type="InstallableUnit">
|
||||
<unit id="org.eclipse.jdt.ls.core" version="0.19.0.201805111532"/>
|
||||
<unit id="org.eclipse.jdt.ls.core.source" version="0.19.0.201805111532"/>
|
||||
<repository location="http://download.eclipse.org/jdtls/snapshots/repository/latest/"/>
|
||||
</location>
|
||||
</locations>
|
||||
<targetJRE path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
|
||||
</target>
|
||||
Reference in New Issue
Block a user