2929import java .util .HashMap ;
3030import java .util .List ;
3131import java .util .Map ;
32- import java .util .Map .Entry ;
3332import java .util .Objects ;
3433import java .util .Timer ;
3534import java .util .TimerTask ;
35+ import java .util .Map .Entry ;
3636import java .util .concurrent .CancellationException ;
3737import java .util .concurrent .CompletableFuture ;
3838import java .util .concurrent .ExecutionException ;
8686import org .eclipse .lsp4j .ClientInfo ;
8787import org .eclipse .lsp4j .CodeActionOptions ;
8888import org .eclipse .lsp4j .CompletionOptions ;
89+ import org .eclipse .lsp4j .DidChangeWatchedFilesParams ;
8990import org .eclipse .lsp4j .DidChangeWorkspaceFoldersParams ;
9091import org .eclipse .lsp4j .DocumentFormattingOptions ;
9192import org .eclipse .lsp4j .DocumentOnTypeFormattingOptions ;
9293import org .eclipse .lsp4j .DocumentRangeFormattingOptions ;
9394import org .eclipse .lsp4j .ExecuteCommandOptions ;
95+ import org .eclipse .lsp4j .FileChangeType ;
96+ import org .eclipse .lsp4j .FileEvent ;
9497import org .eclipse .lsp4j .InitializeParams ;
9598import org .eclipse .lsp4j .InitializeResult ;
9699import 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