From a70731b98b98de6554793d87583fcf6be1b03c1c Mon Sep 17 00:00:00 2001 From: raghucssit Date: Wed, 18 Feb 2026 15:48:15 +0100 Subject: [PATCH] Adapt document to IFile when unable to retrieve file from buffer manager Some framework like Xtext does not connect their document to buffer manager. So LSPEclipseUtils returns null for URI and IFile in such cases. So we can try to adapt the IDocument to URI and IFile. see https://github.com/eclipse-lsp4e/lsp4e/issues/1500 --- .../org/eclipse/lsp4e/LSPEclipseUtils.java | 9 +- .../eclipse/lsp4e/LanguageServerWrapper.java | 151 +++++++++++++++++- 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java index 2a14d8874..2003c41c8 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java @@ -415,7 +415,7 @@ public static CallHierarchyPrepareParams toCallHierarchyPrepareParams(int offset } else if (buffer != null && buffer.getFileStore() != null) { return buffer.getFileStore().toURI(); } - return null; + return Adapters.adapt(document, URI.class, true); } private static @Nullable IPath toPath(@Nullable IFileBuffer buffer) { @@ -1359,7 +1359,12 @@ public static URI toUri(File file) { public static @Nullable IFile getFile(@Nullable IDocument document) { IPath path = toPath(document); - return getFile(path); + IFile file = getFile(path); + //if we cannot determine file via buffer manager then try to adapt from document. + if (file == null) { + file = Adapters.adapt(document, IFile.class, true); + } + return file; } /** diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java index 7830b4b80..fb8f74692 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java @@ -36,6 +36,7 @@ import java.util.TimerTask; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -92,6 +93,7 @@ import org.eclipse.lsp4j.DidChangeWatchedFilesParams; import org.eclipse.lsp4j.DidChangeWatchedFilesRegistrationOptions; import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.DocumentFormattingOptions; import org.eclipse.lsp4j.DocumentOnTypeFormattingOptions; import org.eclipse.lsp4j.DocumentRangeFormattingOptions; @@ -134,6 +136,10 @@ public class LanguageServerWrapper { + // Debounce map: records last time a file buffer listener handled an event for a given URI. + private final ConcurrentHashMap bufferEventTimestamps = new ConcurrentHashMap<>(); + private static final long BUFFER_EVENT_DEBOUNCE_MS = 1000L; // 1 second + private final IFileBufferListener fileBufferListener = new LSFileBufferListener(); private final class LSFileBufferListener extends FileBufferListenerAdapter { @@ -142,6 +148,7 @@ private final class LSFileBufferListener extends FileBufferListenerAdapter { public void bufferDisposed(IFileBuffer buffer) { final var uri = LSPEclipseUtils.toUri(buffer); if (uri != null) { + bufferEventTimestamps.put(uri, System.currentTimeMillis()); disconnect(uri); } } @@ -151,6 +158,10 @@ public void stateChanging(IFileBuffer buffer) { if (buffer.isDirty()) { DocumentContentSynchronizer documentListener = connectedDocuments.get(LSPEclipseUtils.toUri(buffer)); if (documentListener != null) { + final var uri = LSPEclipseUtils.toUri(buffer); + if (uri != null) { + bufferEventTimestamps.put(uri, System.currentTimeMillis()); + } documentListener.documentAboutToBeSaved(); } } @@ -163,6 +174,10 @@ public void dirtyStateChanged(IFileBuffer buffer, boolean isDirty) { } DocumentContentSynchronizer documentListener = connectedDocuments.get(LSPEclipseUtils.toUri(buffer)); if (documentListener != null) { + final var uri = LSPEclipseUtils.toUri(buffer); + if (uri != null) { + bufferEventTimestamps.put(uri, System.currentTimeMillis()); + } documentListener.documentSaved(buffer); } } @@ -177,6 +192,7 @@ public void underlyingFileMoved(IFileBuffer buffer, IPath newPath) { if (documentListener == null) { return; } + bufferEventTimestamps.put(oldUri, System.currentTimeMillis()); LSPEclipseUtils.disconnectFromFileBuffer(buffer.getLocation()); /* * This below is not working (will leak file buffer), because the client that connected the document @@ -205,6 +221,7 @@ public void underlyingFileDeleted(IFileBuffer buffer) { if (oldUri == null) { return; } + bufferEventTimestamps.put(oldUri, System.currentTimeMillis()); if (!isConnectedTo(oldUri)) { return; } @@ -286,6 +303,127 @@ synchronized void close() { private final FileSystemWatcherManager fileSystemWatcherManager; private final WatchedFilesListener watchedFilesListener = new WatchedFilesListener(); + // Fallback workspace listener to catch resource-level events for files that are not backed by file buffers + private final IResourceChangeListener resourceFallbackListener = new ResourceFallbackListener(); + + private final class ResourceFallbackListener implements IResourceChangeListener { + + @Override + public void resourceChanged(IResourceChangeEvent event) { + // Handle PRE_DELETE / PRE_CLOSE that carry the resource directly + if (event.getType() == IResourceChangeEvent.PRE_DELETE + || event.getType() == IResourceChangeEvent.PRE_CLOSE) { + IResource res = event.getResource(); + if (res instanceof IFile file) { + URI uri = LSPEclipseUtils.toUri(file); + if (uri != null) { + // If buffer listener recently handled this URI, skip to avoid duplicate handling + Long last = bufferEventTimestamps.get(uri); + if (last != null && System.currentTimeMillis() - last < BUFFER_EVENT_DEBOUNCE_MS) { + // skip duplicate workspace handling + return; + } + DocumentContentSynchronizer dcs; + boolean wasConnected; + // Read guarded by the same lock used elsewhere when mutating connectedDocuments. + synchronized (connectedDocuments) { + dcs = connectedDocuments.get(uri); + wasConnected = connectedDocuments.containsKey(uri); + } + if (dcs != null) { + // Mirror buffer.stateChanging -> documentAboutToBeSaved + try { + dcs.documentAboutToBeSaved(); + } catch (Exception e) { + LanguageServerPlugin.logError(e); + } + } + if (wasConnected) { + LanguageServerPlugin.logInfo("Workspace PRE_DELETE/PRE_CLOSE disconnect for: " + uri); //$NON-NLS-1$ + disconnectTextFileBuffer(uri); + disconnect(uri); + } + } + } + return; + } + + // For POST_CHANGE examine the delta for file-level removals/moves/replacements and content changes + if (event.getType() != IResourceChangeEvent.POST_CHANGE || event.getDelta() == null) { + return; + } + try { + event.getDelta().accept(delta -> { + IResource r = delta.getResource(); + if (r.getType() != IResource.FILE) { + return true; // continue visiting + } + IFile file = (IFile) r; + int kind = delta.getKind(); + int flags = delta.getFlags(); + System.out.println("Resource change event for " + file.getFullPath() + " with kind " + kind + " and flags " + flags); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + // Content change (save/replace) -> attempt to notify documentSaved + if (kind == IResourceDelta.CHANGED && (flags & IResourceDelta.CONTENT) != 0) { + URI uri = LSPEclipseUtils.toUri(file); + if (uri != null) { + // If buffer listener recently handled this URI, skip to avoid duplicate handling + Long last = bufferEventTimestamps.get(uri); + if (last != null && System.currentTimeMillis() - last < BUFFER_EVENT_DEBOUNCE_MS) { + return false; // skip this file + } + DocumentContentSynchronizer dcs; + // Guard read to avoid concurrent-modification races with writers that synchronize on connectedDocuments + synchronized (connectedDocuments) { + dcs = connectedDocuments.get(uri); + } + if (dcs != null) { + try { + // Mirror buffer.dirtyStateChanged(..., false) -> documentSaved + final var identifier = LSPEclipseUtils.toTextDocumentIdentifier(uri); + final var params = new DidSaveTextDocumentParams(identifier, dcs.getDocument().get()); + // send didSave notification via wrapper to keep ordering + LanguageServerWrapper.this.sendNotification(ls -> ls.getTextDocumentService().didSave(params)); + } catch (Exception e) { + LanguageServerPlugin.logError(e); + } + } + } + } + + // Moved/Removed/Replacement -> disconnect equivalent to underlyingFileMoved/underlyingFileDeleted + if (kind == IResourceDelta.REMOVED + || (kind == IResourceDelta.CHANGED && ((flags & IResourceDelta.MOVED_FROM) != 0 + || (flags & IResourceDelta.MOVED_TO) != 0 + || (flags & IResourceDelta.REPLACED) != 0))) { + URI uri = LSPEclipseUtils.toUri(file); + if (uri != null) { + boolean wasConnected; + // Guard the connected check with the same lock used for mutations + synchronized (connectedDocuments) { + wasConnected = connectedDocuments.containsKey(uri); + } + if (wasConnected) { + // If buffer listener recently handled this URI, skip to avoid duplicate handling + Long last = bufferEventTimestamps.get(uri); + if (last != null && System.currentTimeMillis() - last < BUFFER_EVENT_DEBOUNCE_MS) { + return false; + } + LanguageServerPlugin.logInfo("Workspace resource change disconnect for: " + uri); //$NON-NLS-1$ + disconnectTextFileBuffer(uri); + disconnect(uri); + } + } + } + + return false; // no need to recurse into children of a file + }); + } catch (CoreException e) { + LanguageServerPlugin.logError(e); + } + } + } + /* Backwards compatible constructor */ public LanguageServerWrapper(IProject project, LanguageServerDefinition serverDefinition) { this(project, serverDefinition, null); @@ -514,6 +652,11 @@ private synchronized void start(boolean forceRestart) { } }); FileBuffers.getTextFileBufferManager().addFileBufferListener(fileBufferListener); + // Register a workspace-level fallback listener to catch resource events for + // files not backed by buffers + ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceFallbackListener, + IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE + | IResourceChangeEvent.PRE_CLOSE); castNonNull(initializeFuture).thenRunAsync(() -> { processErrorStream(castNonNull(context.lspStreamProvider), l -> LanguageServerPlugin.getDefault().getLog().error(l), e -> {throw new UncheckedIOException(e);}); }, errorProcessor); @@ -755,6 +898,8 @@ private void shutdown(LanguageServerContext workingContext) { } FileBuffers.getTextFileBufferManager().removeFileBufferListener(fileBufferListener); + ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceFallbackListener); + } public @Nullable CompletableFuture connect(@Nullable IDocument document, IFile file) { @@ -895,6 +1040,10 @@ private boolean supportsWorkspaceFolderCapability() { documentListener.documentClosed(); disconnectTextFileBuffer(uri); } + // Clean up debounce map entry to avoid unbounded growth and avoid stale skips + if (uri != null) { + bufferEventTimestamps.remove(uri); + } if (this.connectedDocuments.isEmpty()) { if (this.serverDefinition.lastDocumentDisconnectedTimeout != 0) { startStopTimerTask(); @@ -1640,4 +1789,4 @@ private static String getThrowableMessage(Throwable throwable) { return message != null ? message : "No exception message available: " + throwable.getClass().getSimpleName(); //$NON-NLS-1$ } -} +} \ No newline at end of file