From 00f433598d04f3baf55e19cc07cb51a520343780 Mon Sep 17 00:00:00 2001 From: Yevhen Vydolob Date: Wed, 16 Aug 2017 10:14:22 +0300 Subject: [PATCH] #1799 implement DidChangeWatchedFiles Notification Signed-off-by: Yevhen Vydolob --- .../languageserver/LanguageServerModule.java | 2 + .../registry/LanguageServerDescription.java | 28 +++++ .../registry/LanguageServerFileWatcher.java | 83 +++++++++++++ .../registry/LanguageServerRegistryImpl.java | 20 +-- .../LanguageServerFileWatcherTest.java | 117 ++++++++++++++++++ 5 files changed, 236 insertions(+), 14 deletions(-) create mode 100644 wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerFileWatcher.java create mode 100644 wsagent/che-core-api-languageserver/src/test/java/org/eclipse/che/api/languageserver/LanguageServerFileWatcherTest.java diff --git a/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/LanguageServerModule.java b/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/LanguageServerModule.java index 23f5c77fa3f..4a6c3a47881 100644 --- a/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/LanguageServerModule.java +++ b/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/LanguageServerModule.java @@ -15,6 +15,7 @@ import org.eclipse.che.api.languageserver.launcher.LanguageServerLauncher; import org.eclipse.che.api.languageserver.messager.PublishDiagnosticsParamsJsonRpcTransmitter; import org.eclipse.che.api.languageserver.messager.ShowMessageJsonRpcTransmitter; +import org.eclipse.che.api.languageserver.registry.LanguageServerFileWatcher; import org.eclipse.che.api.languageserver.registry.LanguageServerRegistry; import org.eclipse.che.api.languageserver.registry.LanguageServerRegistryImpl; import org.eclipse.che.api.languageserver.registry.ServerInitializer; @@ -41,5 +42,6 @@ protected void configure() { Multibinder.newSetBinder(binder(), LanguageDescription.class); bind(LanguageServerInitializationHandler.class).asEagerSingleton(); + bind(LanguageServerFileWatcher.class).asEagerSingleton(); } } diff --git a/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerDescription.java b/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerDescription.java index 9f661459766..d404ace957f 100644 --- a/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerDescription.java +++ b/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerDescription.java @@ -10,17 +10,31 @@ *******************************************************************************/ package org.eclipse.che.api.languageserver.registry; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import static java.util.Collections.emptyList; + public class LanguageServerDescription { private final String id; private final List languageIds; private final List documentFilters; + /** + * The file name patters, format described there {@link java.nio.file.FileSystem#getPathMatcher(String)} + */ + private List fileWatchPatterns = emptyList(); + public LanguageServerDescription(String id, List languageIds, List documentFilters) { + this(id, languageIds, documentFilters, Collections.emptyList()); + } + + public LanguageServerDescription(String id, List languageIds, List documentFilters, List fileWatchPatterns) { this.id = id; this.languageIds = languageIds; this.documentFilters = documentFilters; + this.fileWatchPatterns = fileWatchPatterns; } public String getId() { @@ -35,4 +49,18 @@ public List getDocumentFilters() { return documentFilters; } + + public List getFileWatchPatterns() { + return fileWatchPatterns; + } + + /** + * @param fileWatchPatterns + * must not be null + */ + public void setFileWatchPatterns(List fileWatchPatterns) { + this.fileWatchPatterns = new ArrayList<>(fileWatchPatterns); + } + + } diff --git a/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerFileWatcher.java b/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerFileWatcher.java new file mode 100644 index 00000000000..7d9ed6728d0 --- /dev/null +++ b/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerFileWatcher.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.api.languageserver.registry; + +import com.google.common.annotations.VisibleForTesting; + +import org.eclipse.che.api.languageserver.launcher.LanguageServerLauncher; +import org.eclipse.che.api.vfs.watcher.FileWatcherManager; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.FileChangeType; +import org.eclipse.lsp4j.FileEvent; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.services.LanguageServer; + +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.PathMatcher; +import java.util.Collections; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.eclipse.che.api.languageserver.service.LanguageServiceUtils.prefixURI; + +/** + * Implement DidChangeWatchedFiles + * Notification + */ +@Singleton +public class LanguageServerFileWatcher { + + + private final FileWatcherManager watcherManager; + + private CopyOnWriteArrayList watcherIds = new CopyOnWriteArrayList<>(); + + @Inject + public LanguageServerFileWatcher(FileWatcherManager watcherManager, ServerInitializer serverInitializer) { + this.watcherManager = watcherManager; + serverInitializer.addObserver(this::onServerInitialized); + } + + private void send(LanguageServer server, String filePath, FileChangeType changeType) { + DidChangeWatchedFilesParams params = + new DidChangeWatchedFilesParams(Collections.singletonList(new FileEvent(prefixURI(filePath), changeType))); + server.getWorkspaceService().didChangeWatchedFiles(params); + } + + @PreDestroy + @VisibleForTesting + public void removeAllWatchers() { + for (Integer watcherId : watcherIds) { + watcherManager.unRegisterByMatcher(watcherId); + } + } + + private void onServerInitialized(LanguageServerLauncher launcher, + LanguageServer server, + ServerCapabilities capabilities, + String projectPath) { + LanguageServerDescription description = launcher.getDescription(); + FileSystem fileSystem = FileSystems.getDefault(); + for (String pattern : description.getFileWatchPatterns()) { + PathMatcher matcher = fileSystem.getPathMatcher(pattern); + int watcherId = watcherManager.registerByMatcher(matcher, + s -> send(server, s, FileChangeType.Created), + s -> send(server, s, FileChangeType.Changed), + s -> send(server, s, FileChangeType.Deleted)); + + watcherIds.add(watcherId); + } + } +} diff --git a/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerRegistryImpl.java b/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerRegistryImpl.java index 18f6ad578ed..b9288463a7f 100644 --- a/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerRegistryImpl.java +++ b/wsagent/che-core-api-languageserver/src/main/java/org/eclipse/che/api/languageserver/registry/LanguageServerRegistryImpl.java @@ -106,23 +106,15 @@ public ServerCapabilities initialize(String fileUri) throws LanguageServerExcept for (LanguageServerLauncher launcher : new ArrayList<>(launchers)) { synchronized (initializedServers) { - List servers = launchedServers.get(projectPath); + List servers = launchedServers.computeIfAbsent(projectPath, k -> new ArrayList<>()); - if (servers == null) { - servers = new ArrayList<>(); - launchedServers.put(projectPath, servers); - } - List servers2 = servers; - if (!servers2.contains(launcher)) { - servers2.add(launcher); + if (!servers.contains(launcher)) { + servers.add(launcher); String id = String.valueOf(serverId.incrementAndGet()); initializer.initialize(launcher, new CheLanguageClient(eventService, id), projectPath).thenAccept(pair -> { synchronized (initializedServers) { - List initialized = initializedServers.get(projectPath); - if (initialized == null) { - initialized = new ArrayList<>(); - initializedServers.put(projectPath, initialized); - } + List initialized = + initializedServers.computeIfAbsent(projectPath, k -> new ArrayList<>()); initialized.add(new InitializedLanguageServer(id, pair.first, pair.second, launcher)); launchers.remove(launcher); initializedServers.notifyAll(); @@ -132,7 +124,7 @@ public ServerCapabilities initialize(String fileUri) throws LanguageServerExcept LOG.error("Error launching language server " + launcher, t); synchronized (initializedServers) { launchers.remove(launcher); - servers2.remove(launcher); + servers.remove(launcher); initializedServers.notifyAll(); } return null; diff --git a/wsagent/che-core-api-languageserver/src/test/java/org/eclipse/che/api/languageserver/LanguageServerFileWatcherTest.java b/wsagent/che-core-api-languageserver/src/test/java/org/eclipse/che/api/languageserver/LanguageServerFileWatcherTest.java new file mode 100644 index 00000000000..516db4d7eba --- /dev/null +++ b/wsagent/che-core-api-languageserver/src/test/java/org/eclipse/che/api/languageserver/LanguageServerFileWatcherTest.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.api.languageserver; + +import org.eclipse.che.api.languageserver.launcher.LanguageServerLauncher; +import org.eclipse.che.api.languageserver.registry.LanguageServerDescription; +import org.eclipse.che.api.languageserver.registry.LanguageServerFileWatcher; +import org.eclipse.che.api.languageserver.registry.ServerInitializer; +import org.eclipse.che.api.languageserver.registry.ServerInitializerObserver; +import org.eclipse.che.api.vfs.watcher.FileWatcherManager; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.WorkspaceService; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.io.File; +import java.nio.file.PathMatcher; +import java.util.Collections; +import java.util.function.Consumer; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertTrue; + +/** + * + */ +@Listeners(MockitoTestNGListener.class) +public class LanguageServerFileWatcherTest { + + @Mock + private LanguageServerLauncher launcher; + @Mock + private LanguageServer server; + @Mock + private FileWatcherManager watcherManager; + @Mock + private ServerInitializer initializer; + @Captor + private ArgumentCaptor> changedCaptor; + + private LanguageServerFileWatcher watcher; + + @AfterMethod + public void tearDown() throws Exception { + if (watcher != null) { + watcher.removeAllWatchers(); + } + } + + @Test + public void testShouldAddObserver() throws Exception { + watcher = new LanguageServerFileWatcher(watcherManager, initializer); + verify(initializer).addObserver(any()); + } + + @Test + public void testRegisterFileWatcher() throws Exception { + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ServerInitializerObserver.class); + watcher = new LanguageServerFileWatcher(watcherManager, initializer); + verify(initializer).addObserver(argumentCaptor.capture()); + ServerInitializerObserver value = argumentCaptor.getValue(); + + LanguageServerDescription description = + new LanguageServerDescription("foo", Collections.singletonList("bar"), Collections.emptyList(), + Collections.singletonList("glob:*.foo")); + when(launcher.getDescription()).thenReturn(description); + value.onServerInitialized(launcher, server, null, null); + + ArgumentCaptor pathMatcherCaptor = ArgumentCaptor.forClass(PathMatcher.class); + verify(watcherManager).registerByMatcher(pathMatcherCaptor.capture(), any(), any(), any()); + assertTrue(pathMatcherCaptor.getValue().matches(new File("bar.foo").toPath())); + } + + @Test + public void testSendNotification() throws Exception { + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ServerInitializerObserver.class); + watcher = new LanguageServerFileWatcher(watcherManager, initializer); + verify(initializer).addObserver(argumentCaptor.capture()); + ServerInitializerObserver value = argumentCaptor.getValue(); + + LanguageServerDescription description = + new LanguageServerDescription("foo", Collections.singletonList("bar"), Collections.emptyList(), + Collections.singletonList("glob:*.foo")); + when(launcher.getDescription()).thenReturn(description); + + WorkspaceService workspaceService = mock(WorkspaceService.class); + when(server.getWorkspaceService()).thenReturn(workspaceService); + + value.onServerInitialized(launcher, server, null, null); + + verify(watcherManager).registerByMatcher(any(), any(), changedCaptor.capture(), any()); + + changedCaptor.getValue().accept("/p/bar.foo"); + + verify(workspaceService).didChangeWatchedFiles(any()); + + } + + +}