Skip to content

Commit 33aa2b6

Browse files
committed
feat: add workspace didChangeWatchedFiles support
1 parent 5f622ff commit 33aa2b6

6 files changed

Lines changed: 162 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();

org.eclipse.lsp4e.tests.mock/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
22
Bundle-ManifestVersion: 2
33
Bundle-Name: Mock Language Server to test LSP4E
44
Bundle-SymbolicName: org.eclipse.lsp4e.tests.mock
5-
Bundle-Version: 0.17.0.qualifier
5+
Bundle-Version: 0.17.1.qualifier
66
Bundle-Vendor: Eclipse LSP4E
77
Bundle-RequiredExecutionEnvironment: JavaSE-21
88
Require-Bundle: org.eclipse.lsp4j,

org.eclipse.lsp4e.tests.mock/src/org/eclipse/lsp4e/tests/mock/MockWorkspaceService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class MockWorkspaceService implements WorkspaceService {
3232
private Function<?, ?> _futureFactory;
3333
private CompletableFuture<ExecuteCommandParams> executedCommand = new CompletableFuture<>();
3434
private List<DidChangeWorkspaceFoldersParams> workspaceFoldersEvents = new ArrayList<>();
35+
private List<DidChangeWatchedFilesParams> watchedFilesEvents = new ArrayList<>();
3536

3637
public <U> MockWorkspaceService(Function<U, CompletableFuture<U>> futureFactory) {
3738
this._futureFactory = futureFactory;
@@ -63,6 +64,7 @@ public void didChangeConfiguration(DidChangeConfigurationParams params) {
6364

6465
@Override
6566
public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
67+
watchedFilesEvents.add(params);
6668
}
6769

6870
@Override
@@ -74,6 +76,10 @@ public List<DidChangeWorkspaceFoldersParams> getWorkspaceFoldersEvents() {
7476
return this.workspaceFoldersEvents;
7577
}
7678

79+
public List<DidChangeWatchedFilesParams> getWatchedFilesEvents() {
80+
return this.watchedFilesEvents;
81+
}
82+
7783
@Override
7884
public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
7985
executedCommand.complete(params);

org.eclipse.lsp4e/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
22
Bundle-ManifestVersion: 2
33
Bundle-Name: Language Server Protocol client for Eclipse IDE (Incubation)
44
Bundle-SymbolicName: org.eclipse.lsp4e;singleton:=true
5-
Bundle-Version: 0.19.0.qualifier
5+
Bundle-Version: 0.19.1.qualifier
66
Bundle-RequiredExecutionEnvironment: JavaSE-21
77
Require-Bundle: org.eclipse.core.runtime;bundle-version="3.12.0",
88
org.eclipse.equinox.common;bundle-version="3.8.0",

org.eclipse.lsp4e/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</parent>
1111
<artifactId>org.eclipse.lsp4e</artifactId>
1212
<packaging>eclipse-plugin</packaging>
13-
<version>0.19.0-SNAPSHOT</version>
13+
<version>0.19.1-SNAPSHOT</version>
1414

1515
<build>
1616
<plugins>

org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
import java.util.HashMap;
3030
import java.util.List;
3131
import java.util.Map;
32-
import java.util.Map.Entry;
3332
import java.util.Objects;
3433
import java.util.Timer;
3534
import java.util.TimerTask;
35+
import java.util.Map.Entry;
3636
import java.util.concurrent.CancellationException;
3737
import java.util.concurrent.CompletableFuture;
3838
import java.util.concurrent.ExecutionException;
@@ -86,11 +86,14 @@
8686
import org.eclipse.lsp4j.ClientInfo;
8787
import org.eclipse.lsp4j.CodeActionOptions;
8888
import org.eclipse.lsp4j.CompletionOptions;
89+
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
8990
import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams;
9091
import org.eclipse.lsp4j.DocumentFormattingOptions;
9192
import org.eclipse.lsp4j.DocumentOnTypeFormattingOptions;
9293
import org.eclipse.lsp4j.DocumentRangeFormattingOptions;
9394
import org.eclipse.lsp4j.ExecuteCommandOptions;
95+
import org.eclipse.lsp4j.FileChangeType;
96+
import org.eclipse.lsp4j.FileEvent;
9497
import org.eclipse.lsp4j.InitializeParams;
9598
import org.eclipse.lsp4j.InitializeResult;
9699
import org.eclipse.lsp4j.InitializedParams;
@@ -275,6 +278,8 @@ synchronized void close() {
275278
private final Map<String, Runnable> dynamicRegistrations = new HashMap<>();
276279
private boolean initiallySupportsWorkspaceFolders = false;
277280
private final IResourceChangeListener workspaceFolderUpdater = new WorkspaceFolderListener();
281+
private final IResourceChangeListener watchedFilesListener = new WatchedFilesListener();
282+
private int watchedFilesRegistrationCount = 0;
278283

279284
/* Backwards compatible constructor */
280285
public LanguageServerWrapper(IProject project, LanguageServerDefinition serverDefinition) {
@@ -733,6 +738,8 @@ private void shutdown(LanguageServerContext workingContext) {
733738
this.dynamicRegistrations.clear();
734739

735740
ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceFolderUpdater);
741+
ResourcesPlugin.getWorkspace().removeResourceChangeListener(watchedFilesListener);
742+
watchedFilesRegistrationCount = 0;
736743

737744
CompletableFuture.runAsync(workingContext::close);
738745

@@ -1124,6 +1131,10 @@ public void registerCapability(RegistrationParams params) {
11241131
"Dynamic capability registration failed! Server not yet initialized?"); //$NON-NLS-1$
11251132
params.getRegistrations().forEach(reg -> {
11261133
switch (reg.getMethod()) {
1134+
case "workspace/didChangeWatchedFiles": //$NON-NLS-1$
1135+
addRegistration(reg, this::disableWatchedFiles);
1136+
enableWatchedFiles();
1137+
break;
11271138
case "workspace/didChangeWorkspaceFolders": //$NON-NLS-1$
11281139
if (initiallySupportsWorkspaceFolders) {
11291140
// Can treat this as a NOP since nothing can disable it dynamically if it was
@@ -1225,6 +1236,23 @@ private void addRegistration(Registration reg, Runnable unregistrationHandler) {
12251236
}
12261237
}
12271238

1239+
synchronized void disableWatchedFiles() {
1240+
if (watchedFilesRegistrationCount == 0) {
1241+
return;
1242+
}
1243+
watchedFilesRegistrationCount--;
1244+
if (watchedFilesRegistrationCount == 0) {
1245+
ResourcesPlugin.getWorkspace().removeResourceChangeListener(watchedFilesListener);
1246+
}
1247+
}
1248+
1249+
synchronized void enableWatchedFiles() {
1250+
if (watchedFilesRegistrationCount == 0) {
1251+
ResourcesPlugin.getWorkspace().addResourceChangeListener(watchedFilesListener, IResourceChangeEvent.POST_CHANGE);
1252+
}
1253+
watchedFilesRegistrationCount++;
1254+
}
1255+
12281256
synchronized void setWorkspaceFoldersEnablement(boolean enable) {
12291257
if (enable == supportsWorkspaceFolderCapability()) {
12301258
return;
@@ -1442,6 +1470,90 @@ private boolean isValid(@Nullable WorkspaceFolder wsFolder) {
14421470

14431471
}
14441472

1473+
/**
1474+
* Resource listener that translates Eclipse resource change events into LSP file watch events
1475+
* and dispatches them if the language server is still active
1476+
*/
1477+
private final class WatchedFilesListener implements IResourceChangeListener {
1478+
1479+
@Override
1480+
public void resourceChanged(final IResourceChangeEvent event) {
1481+
List<FileEvent> fileEvents = toFileEvents(event);
1482+
if (fileEvents.isEmpty()) {
1483+
return;
1484+
}
1485+
final LanguageServer currentServer = context.languageServer;
1486+
if (currentServer != null) {
1487+
currentServer.getWorkspaceService()
1488+
.didChangeWatchedFiles(new DidChangeWatchedFilesParams(fileEvents));
1489+
}
1490+
}
1491+
1492+
private List<FileEvent> toFileEvents(final IResourceChangeEvent event) {
1493+
if (event.getType() != IResourceChangeEvent.POST_CHANGE || event.getDelta() == null) {
1494+
return new ArrayList<>();
1495+
}
1496+
1497+
final var relevantFolders = getRelevantWorkspaceFolders();
1498+
final var events = new ArrayList<FileEvent>();
1499+
try {
1500+
event.getDelta().accept(delta -> {
1501+
final IResource resource = delta.getResource();
1502+
if (resource.getType() == IResource.ROOT) {
1503+
return true;
1504+
}
1505+
if (resource.getType() != IResource.FILE) {
1506+
return true;
1507+
}
1508+
if (!(resource instanceof IFile file)) {
1509+
return false;
1510+
}
1511+
1512+
final WorkspaceFolder wsFolder = LSPEclipseUtils.toWorkspaceFolder(file.getProject());
1513+
if (!relevantFolders.contains(wsFolder)) {
1514+
return false;
1515+
}
1516+
1517+
final FileChangeType changeType = getFileChangeType(delta);
1518+
if (changeType == null) {
1519+
return false;
1520+
}
1521+
1522+
final URI uri = LSPEclipseUtils.toUri(file);
1523+
if (uri == null) {
1524+
return false;
1525+
}
1526+
1527+
final var fileEvent = new FileEvent();
1528+
fileEvent.setUri(uri.toASCIIString());
1529+
fileEvent.setType(changeType);
1530+
events.add(fileEvent);
1531+
1532+
return false;
1533+
});
1534+
} catch (final CoreException ex) {
1535+
LanguageServerPlugin.logError(ex);
1536+
}
1537+
return events;
1538+
}
1539+
1540+
private @Nullable FileChangeType getFileChangeType(final IResourceDelta delta) {
1541+
return switch (delta.getKind()) {
1542+
case IResourceDelta.ADDED -> FileChangeType.Created;
1543+
case IResourceDelta.REMOVED -> FileChangeType.Deleted;
1544+
case IResourceDelta.CHANGED -> {
1545+
int flags = delta.getFlags();
1546+
if ((flags & (IResourceDelta.CONTENT | IResourceDelta.REPLACED | IResourceDelta.MOVED_FROM
1547+
| IResourceDelta.MOVED_TO)) != 0) {
1548+
yield FileChangeType.Changed;
1549+
}
1550+
yield null;
1551+
}
1552+
default -> null;
1553+
};
1554+
}
1555+
}
1556+
14451557
/**
14461558
* Extracts the root cause message from a nested exception chain.
14471559
* This helps provide cleaner error messages in dialogs by avoiding
@@ -1464,4 +1576,4 @@ private static String getThrowableMessage(Throwable throwable) {
14641576
return message != null ? message : "No exception message available: " + throwable.getClass().getSimpleName(); //$NON-NLS-1$
14651577
}
14661578

1467-
}
1579+
}

0 commit comments

Comments
 (0)