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