diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllCleanRule.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllCleanRule.java index 3be141135..77fe2d270 100644 --- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllCleanRule.java +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllCleanRule.java @@ -11,17 +11,30 @@ *******************************************************************************/ package org.eclipse.lsp4e.test; +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.lsp4e.LanguageServiceAccessor; import org.eclipse.lsp4e.tests.mock.MockLanguageServer; +import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.intro.IIntroPart; import org.junit.rules.TestWatcher; import org.junit.runner.Description; public class AllCleanRule extends TestWatcher { + + private final Supplier serverConfigurer; + + public AllCleanRule() { + this.serverConfigurer = MockLanguageServer::defaultServerCapabilities; + } + + public AllCleanRule(final Supplier serverConfigurer) { + this.serverConfigurer = serverConfigurer; + } @Override protected void starting(Description description) { @@ -49,6 +62,6 @@ private void clear() { } } LanguageServiceAccessor.clearStartedServers(); - MockLanguageServer.reset(); + MockLanguageServer.reset(this.serverConfigurer); } } diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllTests.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllTests.java index 56493536f..53b8b757d 100644 --- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllTests.java +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/AllTests.java @@ -79,7 +79,8 @@ LanguageServerWrapperTest.class, ColorTest.class, LSPCodeMiningTest.class, - ShowMessageTest.class + ShowMessageTest.class, + WorkspaceFoldersTest.class }) public class AllTests { diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/TestUtils.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/TestUtils.java index 268a95980..05a8f37c3 100644 --- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/TestUtils.java +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/TestUtils.java @@ -23,6 +23,8 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Set; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CountDownLatch; import java.util.function.BooleanSupplier; import org.eclipse.core.resources.IFile; @@ -31,7 +33,6 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jface.text.ITextViewer; -import org.eclipse.ui.tests.harness.util.DisplayHelper; import org.eclipse.lsp4e.ContentTypeToLanguageServerDefinition; import org.eclipse.lsp4e.LanguageServersRegistry; import org.eclipse.lsp4e.tests.mock.MockLanguageServer; @@ -48,6 +49,7 @@ import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.tests.harness.util.DisplayHelper; import org.eclipse.ui.texteditor.AbstractTextEditor; import org.eclipse.ui.texteditor.ITextEditor; @@ -227,4 +229,30 @@ protected boolean condition() { } }.waitForCondition(PlatformUI.getWorkbench().getDisplay(), 1000)); } + + public static class JobSynchronizer extends NullProgressMonitor { + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void done() { + latch.countDown(); + + } + @Override + public void setCanceled(boolean cancelled) { + super.setCanceled(cancelled); + if (cancelled) { + latch.countDown(); + } + } + + public void await() throws InterruptedException, BrokenBarrierException { + latch.await(); + } + + @Override + public void worked(int work) { + latch.countDown(); + } + } } diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/WorkspaceFoldersTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/WorkspaceFoldersTest.java new file mode 100644 index 000000000..4e52664c6 --- /dev/null +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/WorkspaceFoldersTest.java @@ -0,0 +1,221 @@ +/******************************************************************************* + * Copyright (c) 2022 Cocotec Ltd 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 + * + * Contributors: + * Ahmed Hussain (Cocotec Ltd) - initial implementation + * + *******************************************************************************/ +package org.eclipse.lsp4e.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.lsp4e.LanguageServiceAccessor; +import org.eclipse.lsp4e.test.TestUtils.JobSynchronizer; +import org.eclipse.lsp4e.tests.mock.MockLanguageServer; +import org.eclipse.lsp4e.tests.mock.MockWorkspaceService; +import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.WorkspaceFoldersOptions; +import org.eclipse.lsp4j.WorkspaceServerCapabilities; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PlatformUI; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +public class WorkspaceFoldersTest implements Supplier { + + @Rule public AllCleanRule clear = new AllCleanRule(this); + private IProject project; + + @Before + public void setUp() throws CoreException { + MockLanguageServer.INSTANCE.getWorkspaceService().getWorkspaceFoldersEvents().clear(); + project = TestUtils.createProject("WorkspaceFoldersTest" + System.currentTimeMillis()); + } + + @Test + public void testRecycleLSAfterInitialProjectGotDeletedIfWorkspaceFolders() throws Exception { + IFile testFile1 = TestUtils.createUniqueTestFile(project, ""); + + TestUtils.openEditor(testFile1); + LanguageServiceAccessor.getInitializedLanguageServers(testFile1, capabilities -> Boolean.TRUE).iterator() + .next(); + new LSDisplayHelper(() -> MockLanguageServer.INSTANCE.isRunning()).waitForCondition(Display.getCurrent(), 5000, + 300); + + Collection wrappers = LanguageServiceAccessor.getLSWrappers(testFile1, + c -> Boolean.TRUE); + LanguageServerWrapper wrapper1 = wrappers.iterator().next(); + assertTrue(wrapper1.isActive()); + + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().closeAllEditors(false); + new LSDisplayHelper(() -> !MockLanguageServer.INSTANCE.isRunning()).waitForCondition(Display.getCurrent(), 5000, + 300); + + project.delete(true, true, new NullProgressMonitor()); + + project = TestUtils.createProject("LanguageServiceAccessorTest2" + System.currentTimeMillis()); + IFile testFile2 = TestUtils.createUniqueTestFile(project, ""); + + TestUtils.openEditor(testFile2); + LanguageServiceAccessor.getInitializedLanguageServers(testFile2, capabilities -> Boolean.TRUE).iterator() + .next(); + new LSDisplayHelper(() -> MockLanguageServer.INSTANCE.isRunning()).waitForCondition(Display.getCurrent(), 5000, + 300); + + wrappers = LanguageServiceAccessor.getLSWrappers(testFile2, c -> Boolean.TRUE); + LanguageServerWrapper wrapper2 = wrappers.iterator().next(); + assertTrue(wrapper2.isActive()); + + // See corresponding LanguageServiceAccessorTest.testCreateNewLSAfterInitialProjectGotDeleted() - if WorkspaceFolders capability present + // then can recycle the wrapper/server, otherwise a new one gets created + assertTrue(wrapper1 == wrapper2); + } + + @Test + public void testPojectCreate() throws Exception { + IFile testFile1 = TestUtils.createUniqueTestFile(project, ""); + + TestUtils.openEditor(testFile1); + LanguageServiceAccessor.getInitializedLanguageServers(testFile1, capabilities -> Boolean.TRUE).iterator() + .next(); + new LSDisplayHelper(() -> MockLanguageServer.INSTANCE.isRunning()).waitForCondition(Display.getCurrent(), 5000, + 300); + + Collection wrappers = LanguageServiceAccessor.getLSWrappers(testFile1, + c -> Boolean.TRUE); + LanguageServerWrapper wrapper1 = wrappers.iterator().next(); + assertTrue(wrapper1.isActive()); + + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().closeAllEditors(false); + new LSDisplayHelper(() -> !MockLanguageServer.INSTANCE.isRunning()).waitForCondition(Display.getCurrent(), 5000, + 300); + + final MockWorkspaceService mockWorkspaceService = MockLanguageServer.INSTANCE.getWorkspaceService(); + final List events = mockWorkspaceService.getWorkspaceFoldersEvents(); + assertEquals(1, events.size()); + final List added = events.get(0).getEvent().getAdded(); + assertEquals(1, added.size()); + assertEquals(new File(project.getLocationURI()), new File(new URI(added.get(0).getUri()).normalize())); + } + + @Test + public void testProjectClose() throws Exception { + IFile testFile1 = TestUtils.createUniqueTestFile(project, ""); + + TestUtils.openEditor(testFile1); + LanguageServiceAccessor.getInitializedLanguageServers(testFile1, capabilities -> Boolean.TRUE).iterator() + .next(); + new LSDisplayHelper(() -> MockLanguageServer.INSTANCE.isRunning()).waitForCondition(Display.getCurrent(), 5000, + 300); + + final JobSynchronizer synchronizer = new JobSynchronizer(); + project.close(synchronizer); + synchronizer.await(); + + new LSDisplayHelper(() -> MockLanguageServer.INSTANCE.getWorkspaceService().getWorkspaceFoldersEvents().size() == 2).waitForCondition(Display.getCurrent(), 5000, + 300); + final MockWorkspaceService mockWorkspaceService = MockLanguageServer.INSTANCE.getWorkspaceService(); + final List events = mockWorkspaceService.getWorkspaceFoldersEvents(); + assertEquals(2, events.size()); + final List removed = events.get(1).getEvent().getRemoved(); + assertEquals(1, removed.size()); + assertEquals(new File(project.getLocationURI()), new File(new URI(removed.get(0).getUri()))); + } + + @Test + public void testProjectDelete() throws Exception { + IFile testFile1 = TestUtils.createUniqueTestFile(project, ""); + + TestUtils.openEditor(testFile1); + LanguageServiceAccessor.getInitializedLanguageServers(testFile1, capabilities -> Boolean.TRUE).iterator() + .next(); + new LSDisplayHelper(() -> MockLanguageServer.INSTANCE.isRunning()).waitForCondition(Display.getCurrent(), 5000, + 300); + + Collection wrappers = LanguageServiceAccessor.getLSWrappers(testFile1, + c -> Boolean.TRUE); + LanguageServerWrapper wrapper1 = wrappers.iterator().next(); + assertTrue(wrapper1.isActive()); + + // Grab this before deletion otherwise project.getLocationURI will be null... + final File expected = new File(project.getLocationURI()); + final JobSynchronizer synchronizer = new JobSynchronizer(); + project.delete(true, true, synchronizer); + synchronizer.await(); + final MockWorkspaceService mockWorkspaceService = MockLanguageServer.INSTANCE.getWorkspaceService(); + final List events = mockWorkspaceService.getWorkspaceFoldersEvents(); + assertEquals(2, events.size()); + final List removed = events.get(1).getEvent().getRemoved(); + assertEquals(1, removed.size()); + + // Compare files to bodge round URI canonicalization problems + assertEquals(expected, new File(new URI(removed.get(0).getUri()))); + } + + public void projectReopenTest() throws Exception { + IFile testFile1 = TestUtils.createUniqueTestFile(project, ""); + + TestUtils.openEditor(testFile1); + LanguageServiceAccessor.getInitializedLanguageServers(testFile1, capabilities -> Boolean.TRUE).iterator() + .next(); + new LSDisplayHelper(() -> MockLanguageServer.INSTANCE.isRunning()).waitForCondition(Display.getCurrent(), 5000, + 300); + + final JobSynchronizer synchronizer = new JobSynchronizer(); + project.close(synchronizer); + synchronizer.await(); + + new LSDisplayHelper(() -> !project.isOpen()).waitForCondition(Display.getCurrent(), 5000, + 300); + + final JobSynchronizer synchronizer2 = new JobSynchronizer(); + project.open(synchronizer2); + synchronizer2.await(); + + new LSDisplayHelper(() -> project.isOpen()).waitForCondition(Display.getCurrent(), 5000, + 300); + + new LSDisplayHelper(() -> MockLanguageServer.INSTANCE.getWorkspaceService().getWorkspaceFoldersEvents().size() == 3).waitForCondition(Display.getCurrent(), 5000, + 300); + final MockWorkspaceService mockWorkspaceService = MockLanguageServer.INSTANCE.getWorkspaceService(); + final List events = mockWorkspaceService.getWorkspaceFoldersEvents(); + final List added = events.get(2).getEvent().getAdded(); + assertEquals(1, added.size()); + assertEquals(new File(project.getLocationURI()), new File(new URI(added.get(0).getUri()))); + } + + @Override + public ServerCapabilities get() { + // Enable workspace folders on the mock server (for this test only) + final ServerCapabilities base = MockLanguageServer.defaultServerCapabilities(); + + final WorkspaceServerCapabilities wsc = new WorkspaceServerCapabilities(); + final WorkspaceFoldersOptions wso = new WorkspaceFoldersOptions(); + wso.setSupported(true); + wso.setChangeNotifications(true); + wsc.setWorkspaceFolders(wso); + base.setWorkspace(wsc); + return base; + } + +} diff --git a/org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockLanguageServer.java b/org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockLanguageServer.java index 3797872eb..67e47e323 100644 --- a/org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockLanguageServer.java +++ b/org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockLanguageServer.java @@ -22,6 +22,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.function.Supplier; import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeLens; @@ -60,7 +61,7 @@ public final class MockLanguageServer implements LanguageServer { - public static MockLanguageServer INSTANCE = new MockLanguageServer(); + public static MockLanguageServer INSTANCE = new MockLanguageServer(MockLanguageServer::defaultServerCapabilities); /** * This command will be reported on initialization to be supported for execution @@ -77,11 +78,15 @@ public final class MockLanguageServer implements LanguageServer { private List remoteProxies = new ArrayList<>(); public static void reset() { - INSTANCE = new MockLanguageServer(); + INSTANCE = new MockLanguageServer(MockLanguageServer::defaultServerCapabilities); } - private MockLanguageServer() { - resetInitializeResult(); + public static void reset(final Supplier serverConfigurer) { + INSTANCE = new MockLanguageServer(serverConfigurer); + } + + private MockLanguageServer(final Supplier serverConfigurer) { + resetInitializeResult(serverConfigurer); } /** @@ -103,7 +108,24 @@ public void addRemoteProxy(LanguageClient remoteProxy) { this.started = true; } - private void resetInitializeResult() { + private void resetInitializeResult(final Supplier serverConfigurer) { + initializeResult.setCapabilities(serverConfigurer.get()); + } + + CompletableFuture buildMaybeDelayedFuture(U value) { + if (delay > 0) { + return CompletableFuture.runAsync(() -> { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }).thenApply(v -> value); + } + return CompletableFuture.completedFuture(value); + } + + public static ServerCapabilities defaultServerCapabilities() { ServerCapabilities capabilities = new ServerCapabilities(); capabilities.setTextDocumentSync(TextDocumentSyncKind.Full); CompletionOptions completionProvider = new CompletionOptions(false, null); @@ -127,22 +149,8 @@ private void resetInitializeResult() { capabilities.setColorProvider(Boolean.TRUE); capabilities.setDocumentSymbolProvider(Boolean.TRUE); capabilities.setLinkedEditingRangeProvider(new LinkedEditingRangeRegistrationOptions()); - initializeResult.setCapabilities(capabilities); + return capabilities; } - - CompletableFuture buildMaybeDelayedFuture(U value) { - if (delay > 0) { - return CompletableFuture.runAsync(() -> { - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }).thenApply(v -> value); - } - return CompletableFuture.completedFuture(value); - } - @Override public CompletableFuture initialize(InitializeParams params) { return buildMaybeDelayedFuture(initializeResult); diff --git a/org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockWorkspaceService.java b/org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockWorkspaceService.java index 6eacbb894..bad7b4eb0 100644 --- a/org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockWorkspaceService.java +++ b/org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockWorkspaceService.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.eclipse.lsp4e.tests.mock; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -28,6 +29,7 @@ public class MockWorkspaceService implements WorkspaceService { private Function _futureFactory; private CompletableFuture executedCommand = new CompletableFuture<>(); + private List workspaceFoldersEvents = new ArrayList<>(); public MockWorkspaceService(Function> futureFactory) { this._futureFactory = futureFactory; @@ -63,8 +65,11 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { @Override public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { - // TODO Auto-generated method stub + workspaceFoldersEvents.add(params); + } + public List getWorkspaceFoldersEvents() { + return this.workspaceFoldersEvents; } @Override diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java index 3159769d8..67f42d77d 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java @@ -109,6 +109,7 @@ import org.eclipse.lsp4j.UnregistrationParams; import org.eclipse.lsp4j.WorkspaceClientCapabilities; import org.eclipse.lsp4j.WorkspaceEditCapabilities; +import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent; import org.eclipse.lsp4j.WorkspaceFoldersOptions; import org.eclipse.lsp4j.WorkspaceServerCapabilities; @@ -168,13 +169,7 @@ public void dirtyStateChanged(IFileBuffer buffer, boolean isDirty) { */ private final @NonNull Map<@NonNull String, @NonNull Runnable> dynamicRegistrations = new HashMap<>(); private boolean initiallySupportsWorkspaceFolders = false; - private final @NonNull IResourceChangeListener workspaceFolderUpdater = event -> { - WorkspaceFoldersChangeEvent workspaceFolderEvent = toWorkspaceFolderEvent(event); - if (workspaceFolderEvent == null || (workspaceFolderEvent.getAdded().isEmpty() && workspaceFolderEvent.getRemoved().isEmpty())) { - return; - } - this.languageServer.getWorkspaceService().didChangeWorkspaceFolders(new DidChangeWorkspaceFoldersParams(workspaceFolderEvent)); - }; + private final @NonNull IResourceChangeListener workspaceFolderUpdater = new WorkspaceFolderListener(); /* Backwards compatible constructor */ public LanguageServerWrapper(@NonNull IProject project, @NonNull LanguageServerDefinition serverDefinition) { @@ -417,6 +412,7 @@ synchronized void stop() { final Future serverFuture = this.launcherFuture; final StreamConnectionProvider provider = this.lspStreamProvider; final LanguageServer languageServerInstance = this.languageServer; + ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceFolderUpdater); Runnable shutdownKillAndStopFutureAndProvider = () -> { if (languageServerInstance != null) { @@ -454,7 +450,6 @@ synchronized void stop() { this.languageServer = null; FileBuffers.getTextFileBufferManager().removeFileBufferListener(fileBufferListener); - ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceFolderUpdater); } /** @@ -495,38 +490,12 @@ public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { if (currentLS != null && currentLS == LanguageServerWrapper.this.languageServer) { currentLS.getWorkspaceService().didChangeWorkspaceFolders(new DidChangeWorkspaceFoldersParams(wsFolderEvent)); } - ResourcesPlugin.getWorkspace().addResourceChangeListener(workspaceFolderUpdater, IResourceChangeEvent.POST_CHANGE); + ResourcesPlugin.getWorkspace().addResourceChangeListener(workspaceFolderUpdater, IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE); return Status.OK_STATUS; } }.schedule(); } - private static final @Nullable WorkspaceFoldersChangeEvent toWorkspaceFolderEvent(IResourceChangeEvent e) { - if (e.getType() != IResourceChangeEvent.POST_CHANGE) { - return null; - } - WorkspaceFoldersChangeEvent wsFolderEvent = new WorkspaceFoldersChangeEvent(); - try { - e.getDelta().accept(delta -> { - if (delta.getResource().getType() == IResource.PROJECT) { - IProject project = (IProject)delta.getResource(); - if ((delta.getKind() == IResourceDelta.ADDED || delta.getKind() == IResourceDelta.OPEN) && project.isAccessible()) { - wsFolderEvent.getAdded().add(LSPEclipseUtils.toWorkspaceFolder((IProject)delta.getResource())); - } else if (delta.getKind() == IResourceDelta.REMOVED || (delta.getKind() == IResourceDelta.OPEN && !project.isAccessible())) { - wsFolderEvent.getRemoved().add(LSPEclipseUtils.toWorkspaceFolder((IProject)delta.getResource())); - } - // TODO: handle renamed/moved (on filesystem) - } - return delta.getResource().getType() == IResource.ROOT; - }); - } catch (CoreException ex) { - LanguageServerPlugin.logError(ex); - } - if (wsFolderEvent.getAdded().isEmpty() && wsFolderEvent.getRemoved().isEmpty()) { - return null; - } - return wsFolderEvent; - } /** * Check whether this LS is suitable for provided project. Starts the LS if not @@ -875,4 +844,121 @@ public boolean canOperate(@NonNull IDocument document) { return serverDefinition.isSingleton || supportsWorkspaceFolderCapability(); } + /** + * Resource listener that translates Eclipse resource events into LSP workspace folder events + * and dispatches them if the language server is still active + */ + private class WorkspaceFolderListener implements IResourceChangeListener { + @Override + public void resourceChanged(IResourceChangeEvent event) { + WorkspaceFoldersChangeEvent workspaceFolderEvent = toWorkspaceFolderEvent(event); + if (workspaceFolderEvent == null || (workspaceFolderEvent.getAdded().isEmpty() && workspaceFolderEvent.getRemoved().isEmpty())) { + return; + } + // If shutting down, language server will be set to null, so ignore the event + final LanguageServer currentServer = LanguageServerWrapper.this.languageServer; + if (currentServer != null) { + currentServer.getWorkspaceService().didChangeWorkspaceFolders(new DidChangeWorkspaceFoldersParams(workspaceFolderEvent)); + } + } + + private @Nullable WorkspaceFoldersChangeEvent toWorkspaceFolderEvent( + IResourceChangeEvent e) { + if (!isPostChangeEvent(e) && !isPreDeletEvent(e)) { + return null; + } + + // If a project delete then the delta is null, but we get the project in the top-level resource + WorkspaceFoldersChangeEvent wsFolderEvent = new WorkspaceFoldersChangeEvent(); + if (isPreDeletEvent(e)) { + final IResource resource = e.getResource(); + if (resource instanceof IProject) { + wsFolderEvent.getRemoved() + .add(LSPEclipseUtils.toWorkspaceFolder((IProject)resource)); + return wsFolderEvent; + } else { + return null; + } + } + + // Use the visitor implementation to extract the low-level detail from delta + try { + e.getDelta().accept(delta -> { + if (delta.getResource() instanceof IProject) { + IProject project = (IProject) delta.getResource(); + final WorkspaceFolder wsFolder = LSPEclipseUtils.toWorkspaceFolder(project); + if ((isAddEvent(delta) || isProjectOpenCloseEvent(delta)) + && project.isAccessible() + && isValid(wsFolder)) { + wsFolderEvent.getAdded().add(wsFolder); + } else if ((isRemoveEvent(delta)|| isProjectOpenCloseEvent(delta)) + && !project.isAccessible() + && isValid(wsFolder)) { + wsFolderEvent.getRemoved().add(wsFolder); + } + // TODO: handle renamed/moved (on filesystem) + } + return delta.getResource().getType() == IResource.ROOT; + }); + } catch (CoreException ex) { + LanguageServerPlugin.logError(ex); + } + if (wsFolderEvent.getAdded().isEmpty() && wsFolderEvent.getRemoved().isEmpty()) { + return null; + } + return wsFolderEvent; + } + + /** + * + * @return True if this event is being fired after a change (e.g. a project open/close) + */ + private boolean isPostChangeEvent(IResourceChangeEvent e) { + return e.getType() == IResourceChangeEvent.POST_CHANGE; + } + + /** + * + * @return True if this event is being fired prior to a project resource being deleted + */ + private boolean isPreDeletEvent(IResourceChangeEvent e) { + return e.getType() == IResourceChangeEvent.PRE_DELETE; + } + + /** + * + * @return True if this delta corresponds to a project resource being added + */ + private boolean isAddEvent(IResourceDelta delta) { + return delta.getKind() == IResourceDelta.ADDED; + } + + /** + * + * @return True if this delta corresponds to a project resource being removed + */ + private boolean isRemoveEvent(IResourceDelta delta) { + return delta.getKind() == IResourceDelta.REMOVED; + } + + /** + * Decode the bitmask + enum to work out if this is a project open event + * @param delta + * @return True if it is a project open event + */ + private boolean isProjectOpenCloseEvent(IResourceDelta delta) { + return delta.getKind() == IResourceDelta.CHANGED + && (delta.getFlags() & IResourceDelta.OPEN) == IResourceDelta.OPEN; + } + + /** + * + * @return True if this workspace folder is non-null and has non-empty content + */ + private boolean isValid(WorkspaceFolder wsFolder) { + return wsFolder != null && wsFolder.getUri() != null && !wsFolder.getUri().isEmpty(); + } + + } + } \ No newline at end of file