Skip to content

Commit 57dc7a2

Browse files
committed
feat: add workspace didChangeWatchedFiles support
1 parent 5f622ff commit 57dc7a2

11 files changed

Lines changed: 1135 additions & 9 deletions

File tree

org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/commands/DynamicRegistrationTest.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
3131
import org.eclipse.lsp4e.test.utils.TestUtils;
3232
import org.eclipse.lsp4e.tests.mock.MockLanguageServer;
33+
import org.eclipse.lsp4e.tests.mock.MockWorkspaceService;
34+
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
3335
import org.eclipse.lsp4j.ExecuteCommandOptions;
36+
import org.eclipse.lsp4j.FileChangeType;
3437
import org.eclipse.lsp4j.Registration;
3538
import org.eclipse.lsp4j.RegistrationParams;
3639
import org.eclipse.lsp4j.ServerCapabilities;
@@ -48,6 +51,7 @@ public class DynamicRegistrationTest extends AbstractTestWithProject {
4851

4952
private static final String WORKSPACE_EXECUTE_COMMAND = "workspace/executeCommand";
5053
private static final String WORKSPACE_DID_CHANGE_FOLDERS = "workspace/didChangeWorkspaceFolders";
54+
private static final String WORKSPACE_DID_CHANGE_WATCHED_FILES = "workspace/didChangeWatchedFiles";
5155

5256
@BeforeEach
5357
public void setUp() throws Exception {
@@ -73,12 +77,33 @@ public void testCommandRegistration() throws Exception {
7377
assertTrue(LanguageServiceAccessor.hasActiveLanguageServers(handlesCommand("test.command")));
7478
assertTrue(LanguageServiceAccessor.hasActiveLanguageServers(handlesCommand("test.command.2")));
7579
} finally {
76-
unregister(registration);
80+
unregister(registration, WORKSPACE_EXECUTE_COMMAND);
7781
}
7882
assertFalse(LanguageServiceAccessor.hasActiveLanguageServers(handlesCommand("test.command")));
7983
assertFalse(LanguageServiceAccessor.hasActiveLanguageServers(handlesCommand("test.command.2")));
8084
}
8185

86+
@Test
87+
public void testWatchedFilesRegistrationAndNotification() throws Exception {
88+
assertTrue(LanguageServiceAccessor.hasActiveLanguageServers(c -> true));
89+
90+
UUID registration = registerWatchedFiles();
91+
try {
92+
MockWorkspaceService workspaceService = MockLanguageServer.INSTANCE.getWorkspaceService();
93+
94+
TestUtils.createFile(project, "watched.txt", "");
95+
96+
waitForCondition(5_000, () -> !workspaceService.getWatchedFilesEvents().isEmpty());
97+
98+
DidChangeWatchedFilesParams params = workspaceService.getWatchedFilesEvents().get(0);
99+
assertFalse(params.getChanges().isEmpty());
100+
assertTrue(params.getChanges().stream()
101+
.anyMatch(ev -> ev.getUri().endsWith("watched.txt") && ev.getType() == FileChangeType.Created));
102+
} finally {
103+
unregister(registration, WORKSPACE_DID_CHANGE_WATCHED_FILES);
104+
}
105+
}
106+
82107
@Test
83108
public void testWorkspaceFoldersRegistration() throws Exception {
84109
assertTrue(LanguageServiceAccessor.hasActiveLanguageServers(c -> true));
@@ -89,21 +114,31 @@ public void testWorkspaceFoldersRegistration() throws Exception {
89114
try {
90115
assertTrue(LanguageServiceAccessor.hasActiveLanguageServers(c -> hasWorkspaceFolderSupport(c)));
91116
} finally {
92-
unregister(registration);
117+
unregister(registration, WORKSPACE_DID_CHANGE_FOLDERS);
93118
}
94119
assertFalse(LanguageServiceAccessor.hasActiveLanguageServers(c -> hasWorkspaceFolderSupport(c)));
95120
assertTrue(LanguageServiceAccessor.hasActiveLanguageServers(c -> !hasWorkspaceFolderSupport(c)));
96121
}
97122

98123
//////////////////////////////////////////////////////////////////////////////////
99124

100-
private void unregister(UUID registration) throws Exception {
125+
private void unregister(UUID registration, String method) throws Exception {
101126
LanguageClient client = getMockClient();
102-
final var unregistration = new Unregistration(registration.toString(), WORKSPACE_EXECUTE_COMMAND);
127+
final var unregistration = new Unregistration(registration.toString(), method);
103128
client.unregisterCapability(new UnregistrationParams(List.of(unregistration)))
104129
.get(1, TimeUnit.SECONDS);
105130
}
106131

132+
private UUID registerWatchedFiles() throws Exception {
133+
var id = UUID.randomUUID();
134+
LanguageClient client = getMockClient();
135+
final var registration = new Registration();
136+
registration.setId(id.toString());
137+
registration.setMethod(WORKSPACE_DID_CHANGE_WATCHED_FILES);
138+
client.registerCapability(new RegistrationParams(List.of(registration))).get(1, TimeUnit.SECONDS);
139+
return id;
140+
}
141+
107142
private UUID registerWorkspaceFolders() throws Exception {
108143
UUID id = UUID.randomUUID();
109144
LanguageClient client = getMockClient();
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Red Hat, Inc. and others.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* - Angelo ZERR (Red Hat Inc.) - initial API and implementation
11+
* - Sebastian Thomschke (Vegard IT GmbH) - adapted the code from lsp4ij to lsp4e
12+
*******************************************************************************/
13+
package org.eclipse.lsp4e.test.files;
14+
15+
import static org.junit.jupiter.api.Assertions.*;
16+
17+
import java.net.URI;
18+
import java.nio.file.Path;
19+
import java.nio.file.Paths;
20+
import java.util.List;
21+
22+
import org.eclipse.lsp4e.internal.files.FileSystemWatcherManager;
23+
import org.eclipse.lsp4j.FileSystemWatcher;
24+
import org.eclipse.lsp4j.WatchKind;
25+
import org.eclipse.lsp4j.jsonrpc.messages.Either;
26+
import org.junit.jupiter.api.AfterEach;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
30+
// Based on https://github.com/redhat-developer/lsp4ij/blob/6f41f6d22a7146f31e0218cb459513abd5dc16d3/src/test/java/com/redhat/devtools/lsp4ij/features/files/FileSystemWatcherManagerTest.java
31+
32+
/**
33+
* Basic glob pattern tests for {@link FileSystemWatcherManager}.
34+
* <p>
35+
* These are adapted from the LSP4IJ test suite to validate that the
36+
* Eclipse-side implementation behaves consistently for typical patterns and
37+
* watch kinds.
38+
*/
39+
class FileSystemWatcherManagerTest {
40+
41+
private static final String DEFAULT_WATCHER_ID = "default";
42+
43+
private final Path projectDir = Paths.get("current-project").toAbsolutePath();
44+
private final Path externalDir = Paths.get("external-project").toAbsolutePath();
45+
46+
private final FileSystemWatcherManager manager = new FileSystemWatcherManager(projectDir);
47+
48+
@BeforeEach
49+
void setUp() {
50+
manager.clear();
51+
}
52+
53+
@AfterEach
54+
void tearDown() {
55+
manager.clear();
56+
}
57+
58+
@Test
59+
void sapCdsPatterns() {
60+
// Patterns adapted from LSP4IJ sap_cds_ls test
61+
registerWatchers(DEFAULT_WATCHER_ID, List.of( //
62+
new FileSystemWatcher(Either.forLeft("package.json"),
63+
Integer.valueOf(WatchKind.Create | WatchKind.Change | WatchKind.Delete)), new FileSystemWatcher(Either.forLeft("{.git,.cds}ignore"),
64+
Integer.valueOf(WatchKind.Create | WatchKind.Change | WatchKind.Delete)), new FileSystemWatcher(Either.forLeft(".cdsrc.json"),
65+
Integer.valueOf(WatchKind.Create | WatchKind.Change | WatchKind.Delete)), new FileSystemWatcher(Either.forLeft("**/{_i18n,i18n}/i18n{*.properties,*.json,*.csv}"),
66+
Integer.valueOf(WatchKind.Create | WatchKind.Change | WatchKind.Delete))));
67+
68+
// Match package.json at project root
69+
assertMatchFile(projectDir.resolve("package.json").toUri(), WatchKind.Create);
70+
assertMatchFile(projectDir.resolve("package.json").toUri(), WatchKind.Change);
71+
assertMatchFile(projectDir.resolve("package.json").toUri(), WatchKind.Delete);
72+
73+
// Non-matching names/locations
74+
assertNoMatchFile(projectDir.resolve("package.jso").toUri(), WatchKind.Create);
75+
assertNoMatchFile(projectDir.resolve("foo").resolve("package.json").toUri(), WatchKind.Create);
76+
assertNoMatchFile(externalDir.resolve("package.json").toUri(), WatchKind.Create);
77+
78+
// Match {.git,.cds}ignore at project root
79+
assertMatchFile(projectDir.resolve(".gitignore").toUri(), WatchKind.Create);
80+
assertNoMatchFile(projectDir.resolve("gitignore").toUri(), WatchKind.Create);
81+
82+
// Match .cdsrc.json at project root
83+
assertMatchFile(projectDir.resolve(".cdsrc.json").toUri(), WatchKind.Create);
84+
assertNoMatchFile(projectDir.resolve("cdsrc.json").toUri(), WatchKind.Create);
85+
86+
// Match **/{_i18n,i18n}/i18n{*.properties,*.json,*.csv}
87+
assertMatchFile(projectDir.resolve("_i18n").resolve("i18n.properties").toUri(), WatchKind.Create);
88+
assertMatchFile(projectDir.resolve("i18n").resolve("i18n.json").toUri(), WatchKind.Create);
89+
assertNoMatchFile(projectDir.resolve("other").resolve("i18n.properties").toUri(), WatchKind.Create);
90+
}
91+
92+
@Test
93+
void watcherKindFiltering() {
94+
// Register patterns with different explicit kinds
95+
registerWatchers("watcher-kind", List.of(
96+
new FileSystemWatcher(Either.forLeft("**/*.kind_null"), null),
97+
new FileSystemWatcher(Either.forLeft("**/*.kind_7"), Integer.valueOf(7)),
98+
new FileSystemWatcher(Either.forLeft("**/*.kind_Create"), Integer.valueOf(WatchKind.Create)),
99+
new FileSystemWatcher(Either.forLeft("**/*.kind_Change"), Integer.valueOf(WatchKind.Change)),
100+
new FileSystemWatcher(Either.forLeft("**/*.kind_Delete"), Integer.valueOf(WatchKind.Delete))));
101+
102+
URI createUri = projectDir.resolve("foo.kind_Create").toUri();
103+
URI changeUri = projectDir.resolve("foo.kind_Change").toUri();
104+
URI deleteUri = projectDir.resolve("foo.kind_Delete").toUri();
105+
URI nullUri = projectDir.resolve("foo.kind_null").toUri();
106+
URI anyUri = projectDir.resolve("foo.kind_7").toUri();
107+
108+
// kind null -> all kinds
109+
assertMatchFile(nullUri, WatchKind.Create);
110+
assertMatchFile(nullUri, WatchKind.Change);
111+
assertMatchFile(nullUri, WatchKind.Delete);
112+
113+
// kind 7 -> all kinds
114+
assertMatchFile(anyUri, WatchKind.Create);
115+
assertMatchFile(anyUri, WatchKind.Change);
116+
assertMatchFile(anyUri, WatchKind.Delete);
117+
118+
// specific kinds
119+
assertMatchFile(createUri, WatchKind.Create);
120+
assertNoMatchFile(createUri, WatchKind.Change);
121+
assertNoMatchFile(createUri, WatchKind.Delete);
122+
123+
assertNoMatchFile(changeUri, WatchKind.Create);
124+
assertMatchFile(changeUri, WatchKind.Change);
125+
assertNoMatchFile(changeUri, WatchKind.Delete);
126+
127+
assertNoMatchFile(deleteUri, WatchKind.Create);
128+
assertNoMatchFile(deleteUri, WatchKind.Change);
129+
assertMatchFile(deleteUri, WatchKind.Delete);
130+
}
131+
132+
@Test
133+
void globMatchingSimple() {
134+
// Simple glob patterns, adapted from VS Code tests
135+
registerGlobWatcher("node_modules");
136+
assertGlobMatch("node_modules");
137+
assertNoGlobMatch("node_module");
138+
assertNoGlobMatch("test/node_modules");
139+
140+
registerGlobWatcher("test.txt");
141+
assertGlobMatch("test.txt");
142+
143+
// Windows file systems do not allow '?' in file names. Keep the VS Code
144+
// style assertion only on non-Windows platforms.
145+
if (!isWindows()) {
146+
assertNoGlobMatch("test?txt");
147+
}
148+
assertNoGlobMatch("/text.txt");
149+
assertNoGlobMatch("test/test.txt");
150+
}
151+
152+
private void registerWatchers(String id, List<FileSystemWatcher> watchers) {
153+
manager.registerFileSystemWatchers(id, watchers);
154+
}
155+
156+
private void assertMatchFile(URI uri, int kind) {
157+
boolean matched = manager.isMatchFilePattern(uri, kind);
158+
assertTrue(matched, () -> uri + " should match for kind " + kind);
159+
}
160+
161+
private void assertNoMatchFile(URI uri, int kind) {
162+
boolean matched = manager.isMatchFilePattern(uri, kind);
163+
assertFalse(matched, () -> uri + " should not match for kind " + kind);
164+
}
165+
166+
private void registerGlobWatcher(String pattern) {
167+
manager.clear();
168+
manager.registerFileSystemWatchers(DEFAULT_WATCHER_ID,
169+
List.of(new FileSystemWatcher(Either.forLeft(pattern), Integer.valueOf(WatchKind.Create))));
170+
}
171+
172+
private void assertGlobMatch(String relativePath) {
173+
assertGlobMatch(relativePath, true);
174+
}
175+
176+
private void assertNoGlobMatch(String relativePath) {
177+
assertGlobMatch(relativePath, false);
178+
}
179+
180+
private void assertGlobMatch(String relativePath, boolean expected) {
181+
URI uri;
182+
if (relativePath.startsWith("/")) {
183+
uri = projectDir.resolve(relativePath.substring(1)).toUri();
184+
} else {
185+
uri = projectDir.resolve(relativePath).toUri();
186+
}
187+
boolean matched = manager.isMatchFilePattern(uri, WatchKind.Create);
188+
if (expected) {
189+
assertTrue(matched, () -> "Pattern should match " + uri);
190+
} else {
191+
assertFalse(matched, () -> "Pattern should not match " + uri);
192+
}
193+
}
194+
195+
private static boolean isWindows() {
196+
String os = System.getProperty("os.name");
197+
return os != null && os.toLowerCase().contains("win");
198+
}
199+
}

0 commit comments

Comments
 (0)